From 7cc1bb7379acb96095001480cc571839adba371b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jul 2024 18:55:40 +0500 Subject: [PATCH 01/87] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 127 ++++++++++++++++++ core/res/src/main/res/values-ja/strings.xml | 2 + core/res/src/main/res/values-ru/strings.xml | 9 +- .../src/main/res/values-uk-rUA/strings.xml | 66 +++++++++ core/res/src/main/res/values/strings.xml | 5 +- 5 files changed, 205 insertions(+), 4 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 3f0832f3fb..df3279230b 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,8 +1,10 @@ + Netzwerk wählen Benutzerdefiniertes Token hinzufügen Token verwalten Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen. + So scannt man Hilfe anfordern Erneut versuchen Diese Funktion ist im Demomodus deaktiviert @@ -37,6 +39,7 @@ Zu viele Versuche Du hast die biometrische Authentifizierung auf deinem Telefon deaktiviert und kannst keine Wallets in der App speichern. Um Wallets zu speichern, aktiviere bitte die biometrische Authentifizierung in deinen Telefoneinstellungen. Backup-Prozess starten + Von deiner Fiat-Karte oder deinem Bankkonto %d Karte %d Karten @@ -65,6 +68,8 @@ Nicht genug ADA Akzeptieren Zugang verweigert + Alle + Erlauben Anwenden Genehmigung Genehmigen @@ -77,12 +82,17 @@ Gehe zu %1$s D hast keinen Zugang zur Kamera erteilt, bitte passe deine Datenschutzeinstellungen an Abbrechen + Stakingbelohnungen beanstpruchen Schließen Weitermachen Kopieren Adresse kopieren Erstellen Benutzerdefiniert + + Tag + Tage + Entfernen Deaktiviert Erledigt @@ -109,6 +119,7 @@ Weiter Nein Keine Adresse + Jetzt OK Primär Karte Passphrase @@ -139,12 +150,14 @@ Unterstützung Tauschen Allgemeine Geschäftsbedingungen + Heute Transaktion fehlgeschlagen Transaktionen Überweisen Ich verstehe Es ist ein Fehler aufgetreten. Bitte versuche es erneut. Nicht erreichbar + staking beenden Ja Vertragsadresse kopiert! Verfügbare Netzwerke @@ -232,6 +245,7 @@ Daten stammen vom Anbieter. Der geschätzte Betrag kann sich aufgrund der Marktbedingungen ändern. Status des Tausches Verifizierung erforderlich + Wartet auf Transaktions-Hash Liste aller Token, die deiner Wallet hinzugefügt wurden Beste Preise werden abgerufen … Variabler Zinssatz @@ -245,10 +259,12 @@ Erhältlich bei %s Für dieses Paar nicht verfügbar Erlaubnis erforderlich + Empfohlen Nutzungsbedingungen Keine Token gefunden. Bitte versuche eine andere Anfrage ID: %s Transaktions-ID kopiert + Aus einer anderen Währung in deiner Wallet Die folgenden Angaben sind freiwillig. Du kannst diese löschen, wenn du sie nicht weitergeben möchtest. Teile uns mit, welche Funktionen du vermisst, und wir werden versuchen, dir zu helfen. Bitte sag uns, welche Karte du hast @@ -263,6 +279,8 @@ Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. Gib das Genehmigungslimit für das ausgewählte Token an Betrag %s + Die Genehmigungsfunktion ist erforderlich, um einer anderen Adresse die Berechtigung zur Verwendung einer bestimmten Menge Ihrer Token zu erteilen. Standardmäßig können Smart Contracts nicht auf deine Token zugreifen, es sei denn, du stimmen zu. Indem du deine Token \"freischaltest\", autorisierst du den StakeKit Smart Contract, sie zu verwenden. Die Miner des Netzwerks erhalten eine Gasgebühr (von dir bezahlt), um diese Aktion in der Blockchain aufzuzeichnen. Du kannst deine Token einsetzen, nachdem du die Genehmigung erteilt hast. + Um fortzufahren, musst du StakeKit Smart Contract erlauben, deine %s zu verwenden Um fortzufahren, erteile %1s Smart Contracts die Berechtigung, dein zu %2s verwenden. Erlaubnis erteilen Unbegrenzt @@ -306,11 +324,85 @@ %1$d von %2$d Wallet %1$d von %2$d Wallets + Entfernen z.B. BTC vertraue ich, hodl muss ich + Dein Portfolio wurde aktualisiert Der ausgewählte Token ist derzeit nicht für Aktionen innerhalb der Krypto-Wallet verfügbar. Aber keine Sorge, du kannst dein Interesse bekunden, indem du den Token hochstufen. Hochstimmen Wallet auswählen Das Wallet unterstützt nicht mehr als ein Netzwerk + Um mit dem Kauf, Tausch oder Erhalt dieses Vermögenswerts zu beginnen, füge diesen Token zu mindestens 1 Netzwerk hinzu + Dieses Asset ist nicht verfügbar + Zum Portfolio hinzufügen + Token hinzufügen + Verfügbare Netzwerke + Mein Portfolio + Markt + Um Adressen für ausgewählte Netzwerke zu generieren, musst du eine Tangem-Karte scannen/ einsetzen + Die Daten konnten nicht geladen werden... + Schnelle Aktionen + Ergebnis + Token unter 100k Marktkapitalisierung anzeigen + Token anzeigen + Kein Ergebnis + Netzwerk auswählen + Wallet auswählen + 1M + 1 Y + 24H + 3M + 6M + 7T + Alle + Erfahrene Käufer + Bewertung + Sortieren nach + Top-Gewinner + Top-Verlierer + Beliebt + Über %s + + Bewertung, basierend auf %d + Bewertungen, basierend auf %d + + Blockchain-Site + Kaufdruck + Die Differenz zwischen Käufer- und Verkäufervolumen + Umlaufmenge + Die Gesamtzahl der Coins, die für den Handel verfügbar sind und auf dem Markt zirkulieren + Erfahrene Käufer + Nettokäufer mit der zusätzlichen Anforderung, mindestens 100 ausgehende Transaktionen zu haben + Vollständig verwässerte Bewertung + Der theoretische Gesamtwert einer Kryptowährung, wenn alle Coins, die existieren könnten, im Umlauf sind, einschließlich derjenigen, die derzeit nicht im Umlauf sind + Entstehungsdatum + Leer + Hoch + Inhaber/ Halter + Die Änderung der Anzahl der Token-Inhaber innerhalb eines bestimmten Zeitraums + Einblicke + Links + Liquidität + Die Änderung der Liquidität, die dem Token während des angegebenen Zeitraums zur Verfügung steht + Liquiditätsindex + Leer + Niedrig + Marktkapitalisierung + Der Gesamtmarktwert einer Kryptowährung, berechnet durch Multiplikation des aktuellen Preises der Münze mit der Gesamtzahl der im Umlauf befindlichen Münzen + Marktbewertung + Position im Krypto-Rating zwischen allen Coins basierend auf der Marktkapitalisierung + Maximale Versorgung + Leer + Metriken + Offizielle Links + Preisleistung + Aufbewahrungsort + Sicherheitsbewertung + Leer + Soziales + Gesamtangebot + Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können + Handelsvolumen (24h) + Der Gesamtbetrag einer Kryptowährung, der innerhalb der letzten 24 Stunden gehandelt wurde, wobei das Aktivitäts- und Liquiditätsniveau auf dem Markt angegeben wird Du musst einen einzigen Zugangscode einrichten, um alle deine Karten zu schützen Schützen Du kannst später auf jeder Karte einen individuellen Zugangscode einrichten @@ -382,6 +474,7 @@ Karte aktivieren Sicherungskarte Nr. %d Keine Sicherungskarten + Benachrichtigungen Eine Backup-Karte hinzugefügt Bereite deine Karte vor Zwei Backup-Karten hinzugefügt @@ -410,6 +503,7 @@ Kamerazugriff verweigert %1$s ( %2$s ) im %3$s Netzwerk Sende nur %s an diese Adresse. Der Versand einer anderen Währung führt zu ihrem unwiderruflichen Verlust. + QR-Code anzeigen oder Adresse teilen Teilnehmen Die Informationen zum Empfehlungsprogramm konnten nicht geladen werden. Bitte versuche es später noch einmal. Die Informationen über das Empfehlungsprogramm konnten nicht geladen werden. Fehlercode: %s. Bitte versuche es später noch einmal. @@ -470,6 +564,8 @@ Dies sind die Kosten, die du für jede Gaseinheit zu zahlen bereit bist. Je höher der Gaspreis ist, desto schneller wird deine Transaktion bearbeitet. (Vorzugsgebühr inbegriffen) Prioritätsgebühr Die Gebühr, die ein Nutzer an Miner oder Validierer zahlen kann, um die Aufnahme seiner Transaktion in einen Block zu beschleunigen. + Die Gebühr, die für die Nutzung jeder nicht ausgegebenen Transaktionsausgabe (UTXO) im Kaspa-Netzwerk erforderlich ist. Je mehr UTXOs du in einer Transaktion verwenden, desto höher ist die Gebühr. + KAS per UTXO %1$s, %2$s Adresse Ziel-Tag @@ -537,24 +633,46 @@ ≈ %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 + %1s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. Ungültige Adresse %1$s (%2$s) Transaktion gesendet + Bereite das Scannen der Karte vor, die du einrichten möchtest. Entferne diese Wallet Hiermit wird die Wallet aus der Anwendung entfernt. Die Wallet selbst kann wieder hinzugefügt werden. Name + Aktiv + Um deine Kryptos zu unstaken, klick hier. + Die Anzahl der zu stakenden Krypros muss mindesten %s betragen + Effektiver Jahreszins + Jährliche prozentuale Rendite + Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst. Verfügbar Durchschnittliche Belohnungsquote %s geschätzter Profit + Marktbewertung + Metriken Mindestanforderungen Keine Belohnungen zu beanspruchen. Gestaked Belohnungen beanspruchen + Eine Möglichkeit, Staking-Belohnungen zu erhalten. Es kann automatisch oder manuell beansprucht werden. Belohnungszeitplan + Dabei handelt es sich um einen Zeitplan, der festlegt, wann die Teilnehmer am Staking ihre Belohnungen erhalten. + Belohnungen, die du beanspruchen kannst: %s Staking %s Entbindungsdauer + Der Zeitraum, den du nach der Beantragung der Abhebung von Geldern aus dem Staking warten musst, bevor die Token verfügbar werden. Aufwärmphase + Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking. + Natives Staking + Mit Staking kannst du %1s verdienen. Deine Staking-Belohnungen kommen alle ~%2s Tage. + Verdiene Staking-Belohnungen Belohnungen + Mehr staken + unstaken + Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen + Validator/ Prüfer Bewahre deine Krypto-Assets sicher auf, während die privaten Schlüssel auf deiner Karte bleiben Revolutionäre Hardware-Wallet Bis zu 3 physische Karten pro Wallet @@ -566,11 +684,14 @@ Lerne Tangem kennen Tausche, kaufen Sie NFTs, vergebe Kredite und tätige Einlagen bei mehr als 100 verschiedenen dezentralen Diensten Web 3.0-kompatibel + Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche. + Neuer Swap-Anbieter verfügbar! Der Betrag umfasst:\n- Gebühr des Dienstanbieters\n- Netzgebühr für die Rücksendung von %s von der Vermittlungsstelle an die Adresse des Nutzers. Der Betrag enthält die Gebühren des Dienstleisters. Gebühren Alle dezentralen Börsen benötigen Genehmigungen, um zu verhindern, dass intelligente Verträge ohne Ihre Erlaubnis auf Ihre Geldbörse zugreifen. Smart Contracts können nicht auf Ihre Token zugreifen, wenn Sie nicht zustimmen. Indem Sie Ihre Token \"freischalten\", ermächtigen Sie den 1-Zoll-Smart-Contract, sie auszugeben. Die Miner des Netzwerks erhalten eine (von Ihnen bezahlte) Gasgebühr, um diese Aktion in der Blockchain aufzuzeichnen. Sie können Ihre Token tauschen, nachdem Sie Ihre Zustimmung gegeben haben. Genehmigen + Fehler bei der Gebührenschätzung. Bitte sende dein Feedback an den Support. Du wechselst Der Tausch dieser Menge ausgewählter Token hat erhebliche Auswirkungen auf den Preis und verringert dein Ergebnis. Unzureichende Mittel @@ -614,6 +735,7 @@ Operation von: %s zu: %s + Versuche es erneut Du hast dieselbe Karte gescannt. Um ein Zwillings-Wallet zu erstellen, musst du die Karte mit der Nummer %d scannen. Du hast die falsche Doppelkarte gescannt. Bitte versuche eine andere Karte Die, die du in den Hand hältst, und die andere mit der Nummer %s.\n\nBeide Karten können verwendet werden, um Geld aus dieser Wallet zu versenden. @@ -626,6 +748,9 @@ Diese Aktion ist unumkehrbar. Du hast keinen Zugriff mehr auf die alte Wallet. Tippe auf die Doppelkarte mit der Nummer %s und entferne sie erst am Ende des Vorgangs. Verwende %s oder scanne eine Karte, um Zugriff auf deine Wallet zu erhalten. + Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten + Sei der Erste, der von neuen Aktionen erfährt + Möchtest du Push-Benachrichtigungen verwenden? Neues Wallet hinzufügen Möchtest du diese Wallet wirklich löschen? Es ist ein Fehler aufgetreten, bitte scanne deine Karte, um sich anzumelden @@ -667,6 +792,7 @@ WalletConnect-Sitzungen Mit dApps verbinden WalletConnect + Das Herstellen der Verbindung kann einige Sekunden dauern %s Marktpreis letzte 24h %s Netzwerk @@ -675,6 +801,7 @@ Wallet-Einstellungen Tangem Verwende %s oder scanne eine Karte, um den Zugriff auf deine Wallet freizuschalten. + Es scheint, dass die Aktivierung der Karte nicht korrekt abgeschlossen wurde. Dies kann an einem Problem mit dem NFC-Modul deines Gerätes oder an einem falschen Tippen der Karte auf dein Gerät liegen. Bitte wende dich an unser Support-Team, um Unterstützung zu erhalten. Aktivierungsfehler Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen. BNB Beacon Chain wird abgeschaltet diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index e004bcbd28..de7d3bc1e3 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -621,6 +621,7 @@ ≈ %1$s (%2$s: 手数料を含む) %sが送信されます 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 + %1sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 無効なアドレス %1$s ( %2$s ) 取引が送信されました @@ -788,6 +789,7 @@ ウォレット設定 Tangem %sを使用するか、カードをスキャンしてウォレットにアクセスしてください + カードのアクティベーションが正しく完了しませんでした。デバイスの NFCモジュールに問題があるか、カードをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。 アクティベーションに失敗しました BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください。 BNBビーコンチェーンは閉鎖されます。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index aa18939439..9784dd7cc8 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -124,6 +124,7 @@ Основная карта Парольная фраза Вставить + %1$s-%2$s Подробнее Получить Отклонить @@ -424,7 +425,7 @@ Код доступа Группы По балансу - Сортировка токенов + Упорядочить токены Список Выбрать из галереи Настройки @@ -564,16 +565,18 @@ ≈ %1$s (вкл. комиссию: %2$s) %s будет отправлено Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время - %1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес. + %1s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес. Неверный адрес Транзакция отправлена Подготовьтесь к сканированию карты, которую вы хотите настроить. Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя + Сумма для стейкинда должна быть не менее %s APY Годовой процентный доход, который вы можете получить от участия в стейкинге. Доступно + %s Способ возраграждения Способ получения вознаграждений за стейкинг. Он может быть автоматическим или ручным. Период возрагражения @@ -710,7 +713,7 @@ Настройки кошелька Tangem Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку - Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян. + Похоже, что процесс активации карт не был завершен корректно. Это могло быть вызвано проблемой взаимодействия с модулем NFC либо некорректным прикладыванием карты к телефону. Пожалуйста, обратитесь в нашу службу поддержки для уточнения деталей. Ошибка активации По решению разработчиков сети BNB стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении или сторонние сервисы, чтобы перевести средства в cеть BNB Smart Chain. Отключение сети BNB Beacon Chain diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index c6e93b561a..57f722155b 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -39,6 +39,7 @@ Забагато спроб Ви вимкнули біометричну автентифікацію на своєму телефоні і не зможете зберігати гаманці в додатку. Щоб зберегти гаманці, будь ласка, увімкніть функцію біометричної автентифікації в налаштуваннях телефону. Почніть процес резервного копіювання + З вашої фіатної картки або банківського рахунку %d картка %d картки @@ -83,6 +84,7 @@ Перейдіть до %1$s Ви не надали доступ до камери, будь ласка, змініть налаштування конфіденційності Скасувати + Отримати винагороди Закрити Продовжити Копіювати @@ -159,6 +161,7 @@ Я зрозумів Виникла помилка. Будь ласка, спробуйте ще раз. Недоступно + Скасувати стейкінг Так Адреса контракту скопійована! Доступні мережі @@ -246,6 +249,7 @@ Дані провайдера. Орієнтовна сума може бути змінена через ринкові умови. Статус обміну Потрібна верифікація + Очікування хешу транзакції Список токенів у вашому гаманцю Шукаємо найвигідніший курс... Плаваюча ставка @@ -259,10 +263,12 @@ Доступно від %s Недоступно для цієї пари Потрібен дозвіл + Рекомендовано Умовами використання Токенів не знайдено. Будь ласка, спробуйте інший запит ID: %s ID транзакції скопійовано + З іншої валюти у вашому гаманці Інформація нижче не є обов\'язковою. Ви можете стерти її, якщо бажаєте. Розкажіть, яких функцій вам не вистачає, і ми спробуємо вам допомогти. Розкажіть, будь ласка, яку картку ви маєте? @@ -277,6 +283,8 @@ Мережа стягує комісію за схвалення токену за підтвердження, що саме ви дозволяєте використовувати ваш токен для обміну. Вкажіть ліміт доступу для обраного токена Кількість %s + Функція підтвердження необхідна для надання дозволу іншій адресі на використання певної кількості ваших токенів. За задумом, смарт-контракти не можуть отримати доступ до ваших токенів без вашого схвалення. \"Розблоковуючи\" свої токени, ви дозволяєте смарт-контракту StakeKit використовувати їх. Майнери мережі отримують плату за газ (сплачену вами), щоб зафіксувати цю дію в блокчейні. Ви зможете застейкати свій токен після того, як дасте дозвіл. + Щоб продовжити, вам потрібно дозволити смарт-контракту StakeKit використовувати ваш %s Щоб продовжити, вам потрібно надати дозвіл смарт-контракту %1s використовувати ваш %2s Надати дозвіл Необмежено @@ -332,20 +340,73 @@ Щоб почати купувати, обмінювати або отримувати цей актив, додайте цей токен принаймні в 1 мережу Цей актив недоступний Додати в портфоліо + Додати токен + Доступні мережі Моє портфоліо Маркет + Щоб згенерувати адреси для обраних мереж, потрібно прикласти картку Tangem Не вдалося завантажити дані... + Швидкі дії Результат Переглянути токени до 100к ринкової капіталізації Показати токени Жодного результату + Виберіть мережу Оберіть гаманець + 1 міс. + 1 рік + 24 год. + 3 міс. + 6 міс. + 7 днів + Увесь + Досвідчені покупці + За рейтингом Сортувати за + Лідери росту + Лідери падіння + В тренді + Про %s + + На основі %d оцінки + На основі %d оцінок + На основі %d оцінок + На основі %d оцінок + + Блокчейн сайт + Давлення покупця + Різниця між обсягом покупців та обсягом продавців + Циркуляційний запас + Загальна кількість монет, які доступні для торгівлі та перебувають в обігу на ринку + Досвідчені покупці + Мережеві покупці з додатковою вимогою мати не менше 100 вихідних транзакцій + Повністю розведена ринкова капіталізація + Загальна теоретична вартість криптовалюти, якщо всі монети, які могли б існувати, перебувають в обігу, включаючи ті, що не перебувають в обігу в даний час + Дата створення + Високий + Тримачі + Зміна кількості власників токенів протягом певного періоду часу Інсайти Посилання + Ліквідність + Зміна того, скільки ліквідності доступно для токена протягом зазначеного періоду часу + Індекс ліквідності + Низький + Ринкова капіталізація + Загальна ринкова вартість криптовалюти, що розраховується шляхом множення поточної ціни монети на загальну кількість монет в обігу + Рейтинг ринку + Позиція в крипторейтингу між усіма монетами на основі ринкової капіталізації + Максимальна пропозиція Метрики + Офіційні посилання Цінова ефективність + Репозиторій Оцінка безпеки + Соцмережі + Загальна пропозиція + Максимальна кількість монет або токенів, яка може коли-небудь існувати для певної криптовалюти + Обсяг торгів (24г) + Загальна сума криптовалюти, якою торгували протягом останніх 24 годин, що вказує на рівень активності та ліквідності на ринку Вам потрібно встановити єдиний код доступу для захисту всіх ваших карток Захист Пізніше ви зможете налаштувати індивідуальний код доступу до кожної картки @@ -450,6 +511,7 @@ Доступ до камери заборонено %1$s (%2$s) у мережі %3$s Надсилайте лише %s на цю адресу. Надсилання будь-якої іншої валюти призведе до її незворотної втрати. + Покажіть QR-код або поділіться своєю адресою Взяти участь Не вдалося завантажити інформацію по реферальній програмі. Будь ласка, спробуйте пізніше. Не вдалося завантажити інформацію по реферальній програмі. Код помилки: %s. Будь ласка, спробуйте пізніше. @@ -583,6 +645,7 @@ ≈ %1$s (вкл. комісію: %2$s ) %s буде надіслано Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час + %1s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron(TRX) на свій рахунок. Недійсна адреса %1$s (%2$s) Трансакцію надіслано @@ -640,6 +703,7 @@ Комісії Всі децентралізовані біржі вимагають схвалення, щоб запобігти доступу смарт-контрактів до вашого гаманця без вашого дозволу. За задумом смарт-контракти не можуть отримати доступ до ваших токенів без вашого схвалення. \"Розблоковуючи\" свої токени, ви дозволяєте смарт-контракту 1inch витрачати ваші активи. Майнери мережі отримують плату за газ (сплачену вами), щоб зафіксувати цю дію в блокчейні. Ви можете обміняти свій токен після того, як дасте дозвіл. Підтвердити + Помилка при розрахунку комісії. Будь ласка, надішліть відгук до служби підтримки. Ви обмінюєте Обмін цієї кількості обраних токенів призведе до значного впливу на ціну і зменшить вашу кінцеву суму. Недостатньо коштів @@ -697,6 +761,7 @@ Прикладіть twin-картку з номером %s та не прибирайте її до завершення операції Використовуйте %s або відскануйте картку, щоб отримати доступ до свого гаманця Будьте в курсі останніх функцій та новин + Дізнавайтеся першими про нові акції Бажаєте використовувати Push-повідомлення? Додати новий гаманець Ви впевнені, що хочете видалити цей гаманець? @@ -748,6 +813,7 @@ Налаштування гаманця Tangem Використовуйте %s або відскануйте картку, щоб розблокувати доступ до гаманця + Схоже, що активація картки була виконана неправильно. Це може бути пов\'язано з проблемою з модулем NFC вашого пристрою або неправильним прикладанням картки до пристрою. Зверніться за допомогою до нашої служби підтримки. Помилка активації За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain. Відключення мережі BNB Beacon Chain diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 954e7e59ca..4172b1298d 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -82,6 +82,7 @@ Go to %1$s You have not given access to your camera, please adjust your privacy settings Cancel + Claim rewards Close Continue Copy @@ -156,6 +157,7 @@ I understand There was an error. Please try again. Unreachable + Unstake Yes Contract address copied! Available networks @@ -637,6 +639,7 @@ Name Active To unstake your assets, click here. + The amount to stake must be at least %s APR APY The annual percentage return you can earn from participating in staking. @@ -795,7 +798,7 @@ Wallet settings 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. + It seems that the card activation was not completed correctly. This could be due to an issue with your device\'s NFC module or incorrect tapping of the card to your device. Please contact our Support team for assistance. Activation error According to BNB network developers, support for the BEP-2 standard will end in June 2024. To avoid losing assets with this standard, please convert them to the BEP-20 standard. Use our swap service or third-party services to transfer funds to the BNB Smart Chain network. BNB Beacon Chain will shut down From ea301764901f87751ee9eea6843a12ccb86339bb Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jul 2024 18:55:55 +0500 Subject: [PATCH 02/87] Updated on 2026-08-14 --- .../converters/AmountReduceByTransformer.kt | 3 + .../converters/AmountReduceToTransformer.kt | 3 + .../field/AmountFieldChangeTransformer.kt | 3 + .../common/ui/amountScreen/ui/AmountField.kt | 15 +++-- .../response/model/AddressArgumentDTO.kt | 5 +- .../data/staking/converters/YieldConverter.kt | 16 +++++- .../domain/staking/model/stakekit/Yield.kt | 14 +++-- .../SetInitialDataStateTransformer.kt | 3 +- .../amount/AmountChangeStateTransformer.kt | 17 +++++- .../amount/AmountMaxValueStateTransformer.kt | 18 +++++- .../AmountRequirementStateTransformer.kt | 55 +++++++++++++++++++ .../ui/StakingInitialInfoContent.kt | 23 ++++---- .../ui/StakingNavigationButtons.kt | 16 ++---- .../viewmodel/StakingViewModel.kt | 8 +-- 14 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt index 78ce58f37d..9597b3d764 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt @@ -2,10 +2,12 @@ package com.tangem.common.ui.amountScreen.converters import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.utils.checkExceedBalance import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero @@ -51,6 +53,7 @@ class AmountReduceByTransformer( value = cryptoValue, fiatValue = fiatValue, isError = isExceedBalance, + error = resourceReference(R.string.send_validation_amount_exceeds_balance), cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), keyboardOptions = KeyboardOptions( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt index aed8332b0b..c81634b818 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt @@ -2,10 +2,12 @@ package com.tangem.common.ui.amountScreen.converters import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.utils.checkExceedBalance import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero @@ -45,6 +47,7 @@ class AmountReduceToTransformer( value = cryptoValue, fiatValue = fiatValue, isError = isExceedBalance, + error = resourceReference(R.string.send_validation_amount_exceeds_balance), cryptoAmount = amountTextField.cryptoAmount.copy(value = value), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), keyboardOptions = KeyboardOptions( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index f0be223891..d45e7c07bd 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -2,11 +2,13 @@ package com.tangem.common.ui.amountScreen.converters.field import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.utils.checkExceedBalance import com.tangem.common.ui.amountScreen.utils.getCryptoValue import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero @@ -59,6 +61,7 @@ class AmountFieldChangeTransformer( value = cryptoValue, fiatValue = fiatValue, isError = isExceedBalance, + error = resourceReference(R.string.send_validation_amount_exceeds_balance), cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), keyboardOptions = KeyboardOptions( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index 1144bf4bcd..f190cea1db 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -1,16 +1,16 @@ package com.tangem.common.ui.amountScreen.ui import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize 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.requiredHeightIn +import androidx.compose.foundation.layout.* 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 @@ -80,6 +80,8 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount Box( modifier = Modifier + .fillMaxWidth() + .animateContentSize() .padding( top = TangemTheme.dimens.spacing8, start = TangemTheme.dimens.spacing12, @@ -105,7 +107,7 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, modifier = Modifier - .align(BottomCenter) + .align(TopCenter) .padding(bottom = TangemTheme.dimens.spacing32), ) AmountFieldError( @@ -113,7 +115,10 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri error = amountField.error, modifier = Modifier .align(BottomCenter) - .padding(bottom = TangemTheme.dimens.spacing12), + .padding( + top = TangemTheme.dimens.spacing20, + bottom = TangemTheme.dimens.spacing12, + ), ) } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt index 7a4f33b288..6138444c45 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.stakekit.models.response.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import java.math.BigDecimal @JsonClass(generateAdapter = true) data class AddressArgumentDTO( @@ -10,7 +11,7 @@ data class AddressArgumentDTO( @Json(name = "network") val network: String? = null, @Json(name = "minimum") - val minimum: Double? = null, + val minimum: BigDecimal? = null, @Json(name = "maximum") - val maximum: Double? = null, + val maximum: BigDecimal? = null, ) \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index ddf093c628..f19e97319c 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -39,14 +39,18 @@ class YieldConverter( private fun convertEnter(enterDTO: YieldDTO.ArgsDTO.Enter): Yield.Args.Enter { return Yield.Args.Enter( addresses = convertAddresses(enterDTO.addresses), - args = enterDTO.args.mapValues { convertAddressArgument(it.value) }, + args = enterDTO.args + .mapKeys { convertArgType(it.key) } + .mapValues { convertAddressArgument(it.value) }, ) } private fun convertAddresses(addressesDTO: YieldDTO.ArgsDTO.Enter.Addresses): Yield.Args.Enter.Addresses { return Yield.Args.Enter.Addresses( address = convertAddressArgument(addressesDTO.address), - additionalAddresses = addressesDTO.additionalAddresses?.mapValues { convertAddressArgument(it.value) }, + additionalAddresses = addressesDTO.additionalAddresses + ?.mapKeys { convertArgType(it.key) } + ?.mapValues { convertAddressArgument(it.value) }, ) } @@ -122,4 +126,12 @@ class YieldConverter( else -> Yield.RewardType.UNKNOWN } } + + private fun convertArgType(value: String): Yield.Args.ArgType { + return when (value) { + "address" -> Yield.Args.ArgType.ADDRESS + "amount" -> Yield.Args.ArgType.AMOUNT + else -> Yield.Args.ArgType.UNKNOWN + } + } } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt index e173689c6f..f30b65fcf4 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt @@ -33,15 +33,21 @@ data class Yield( @Serializable data class Enter( val addresses: Addresses, - val args: Map, + val args: Map, ) { @Serializable data class Addresses( val address: AddressArgument, - val additionalAddresses: Map? = null, + val additionalAddresses: Map? = null, ) } + + enum class ArgType { + ADDRESS, + AMOUNT, + UNKNOWN, + } } @Serializable @@ -113,6 +119,6 @@ data class Token( data class AddressArgument( val required: Boolean, val network: String? = null, - val minimum: Double? = null, - val maximum: Double? = null, + val minimum: SerializedBigDecimal? = null, + val maximum: SerializedBigDecimal? = null, ) \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 7e45dc8557..6023ae90fb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -127,7 +127,7 @@ internal class SetInitialDataStateTransformer( startText = TextReference.Res(R.string.staking_details_minimum_requirement), endText = TextReference.Str( value = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = yield.args.enter.args[KEY_AMOUNT]?.minimum?.toBigDecimal(), + cryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum, cryptoCurrency = cryptoCurrencyStatus.currency.symbol, decimals = cryptoCurrencyStatus.currency.decimals, ), @@ -191,6 +191,5 @@ internal class SetInitialDataStateTransformer( companion object { private val EQUALITY_THRESHOLD = BigDecimal(1E-10) - private const val KEY_AMOUNT = "amount" } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt index 110de484fc..4efdb9141d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -1,18 +1,33 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer internal class AmountChangeStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val yield: Yield, private val value: String, ) : Transformer { + private val amountRequirementStateTransformer by lazy(LazyThreadSafetyMode.NONE) { + AmountRequirementStateTransformer( + cryptoCurrencyStatus, + yield, + value, + ) + } + override fun transform(prevState: StakingUiState): StakingUiState { + val updatedAmountState = AmountFieldChangeTransformer( + cryptoCurrencyStatus, + value, + ).transform(prevState.amountState) + return prevState.copy( - amountState = AmountFieldChangeTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + amountState = amountRequirementStateTransformer.transform(updatedAmountState), ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt index 3d7dc9125d..8270ef3971 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -1,16 +1,32 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.field.AmountFieldMaxAmountTransformer +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer internal class AmountMaxValueStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val yield: Yield, ) : Transformer { + + private val amountRequirementStateTransformer by lazy(LazyThreadSafetyMode.NONE) { + val value = cryptoCurrencyStatus.value.amount + ?.parseBigDecimal(cryptoCurrencyStatus.currency.decimals) + .orEmpty() + AmountRequirementStateTransformer( + cryptoCurrencyStatus, + yield, + value, + ) + } + override fun transform(prevState: StakingUiState): StakingUiState { + val updatedAmountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState) return prevState.copy( - amountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState), + amountState = amountRequirementStateTransformer.transform(updatedAmountState), ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt new file mode 100644 index 0000000000..72d7a6cb2d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -0,0 +1,55 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.extensions.isZero +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.R +import com.tangem.utils.transformer.Transformer + +internal class AmountRequirementStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val yield: Yield, + private val value: String, +) : Transformer { + override fun transform(prevState: AmountState): AmountState { + val amountRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT] + val amountDecimal = value.parseToBigDecimal(cryptoCurrencyStatus.currency.decimals) + + return if (prevState !is AmountState.Data || amountRequirements == null) { + prevState + } else { + val isAlreadyErrorState = prevState.amountTextField.isError + val isAmountRequired = amountRequirements.required + val isAmountZero = amountDecimal.isZero() + val isExceedsRequirements = + amountRequirements.maximum?.compareTo(amountDecimal) == -1 || + amountRequirements.minimum?.compareTo(amountDecimal) == 1 + + val isRequirementError = !isAmountZero && isAmountRequired && isExceedsRequirements && !isAlreadyErrorState + if (isRequirementError) { + prevState.copy( + amountTextField = prevState.amountTextField.copy( + isError = true, + error = resourceReference( + R.string.staking_amount_requirement_error, + wrappedList( + BigDecimalFormatter.formatCryptoAmount( + amountRequirements.minimum, + cryptoCurrencyStatus.currency.symbol, + cryptoCurrencyStatus.currency.decimals, + ), + ), + ), + ), + ) + } else { + prevState + } + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 859bc837c1..961cdb9643 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -67,7 +67,7 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl ) } } - AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") { + AnimatedContent(targetState = state.yieldBalance, label = "Staking yield visibility animation") { if (it is InnerYieldBalanceState.Data) { ActiveStakingBlock(it.balance, clickIntents::onActiveStake) } @@ -168,6 +168,7 @@ private fun StakingRewardBlock( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), + enabled = isRewardsToClaim, onClick = onRewardsClick, ), ) @@ -192,18 +193,20 @@ private fun ActiveStakingBlock(groups: List, onClick: (Bala ) { group.items.forEachIndexed { index, balance -> key(balance.validator.address) { - val caption = combinedReference( - if (group.type == BalanceGroupType.UNSTAKED) { - resourceReference(R.string.staking_details_unbonding_period) + val caption = if (group.type == BalanceGroupType.UNSTAKED) { + combinedReference( + resourceReference(R.string.staking_details_unbonding_period), annotatedReference { appendSpace() appendColored( text = balance.unbondingPeriod.resolveReference(), color = TangemTheme.colors.text.accent, ) - } - } else { - resourceReference(R.string.app_name) + }, + ) + } else { + combinedReference( + resourceReference(R.string.app_name), annotatedReference { appendSpace() appendColored( @@ -213,9 +216,9 @@ private fun ActiveStakingBlock(groups: List, onClick: (Bala ), color = TangemTheme.colors.text.accent, ) - } - }, - ) + }, + ) + } InputRowImageInfo( title = group.title.takeIf { index == 0 }, subtitle = stringReference(balance.validator.name), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt index 1ce479e4a5..67a7000962 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -148,17 +148,10 @@ private fun getButtonData(currentState: StakingUiState): Int { private fun onPrimaryClick(currentState: StakingUiState) { when (currentState.currentStep) { - StakingStep.InitialInfo -> { - val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data - if (initialState?.yieldBalance is InnerYieldBalanceState.Data) { - if (initialState.isStakeMoreAvailable) { - currentState.clickIntents.onNextClick() - } - } else { - currentState.clickIntents.onNextClick() - } - } - StakingStep.Amount -> currentState.clickIntents.onNextClick() + StakingStep.InitialInfo, + StakingStep.Validators, + StakingStep.Amount, + -> currentState.clickIntents.onNextClick() StakingStep.Confirmation -> { val confirmationState = currentState.confirmationState if (confirmationState is StakingStates.ConfirmationState.Data) { @@ -171,7 +164,6 @@ private fun onPrimaryClick(currentState: StakingUiState) { currentState.clickIntents.onBackClick() } } - StakingStep.Validators -> currentState.clickIntents.onNextClick() StakingStep.RewardsValidators -> Unit } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 1aa55e58c3..5459256c1d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -196,7 +196,7 @@ internal class StakingViewModel @Inject constructor( } override fun onAmountValueChange(value: String) { - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, value)) + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, value)) } override fun onAmountPasteTriggerDismiss() { @@ -204,7 +204,7 @@ internal class StakingViewModel @Inject constructor( } override fun onMaxValueClick() { - stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus)) + stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus, yield)) } override fun onCurrencyChangeClick(isFiat: Boolean) { @@ -223,7 +223,7 @@ internal class StakingViewModel @Inject constructor( } override fun selectRewardValidator(rewardValue: String) { - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, rewardValue)) + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, rewardValue)) onNextClick() } @@ -234,7 +234,7 @@ internal class StakingViewModel @Inject constructor( RouteType.OTHER } stateController.update { it.copy(routeType = routeType) } - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue)) + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, activeStake.cryptoValue)) onNextClick(activeStake.pendingActions) } From f3ce7fd4bc16d50655b22c292d45841e77a0ec55 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jul 2024 20:37:19 +0300 Subject: [PATCH 03/87] Updated on 2026-08-14 --- version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.properties b/version.properties index f8c3184558..16ef03c0ac 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -versionName=5.13.0 \ No newline at end of file +versionName=5.14.0 \ No newline at end of file From 258af2284223737299e3290b50f700f827abec8f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Jul 2024 18:01:00 +0300 Subject: [PATCH 04/87] Updated on 2026-08-14 --- .../{ScanErrorTest.kt => ScanEmptyTest.kt} | 0 core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-fr/strings.xml | 3 + core/res/src/main/res/values-ja/strings.xml | 2 +- core/res/src/main/res/values-ru/strings.xml | 2 +- .../src/main/res/values-uk-rUA/strings.xml | 2 +- .../src/main/res/values-zh-rTW/strings.xml | 8 +- .../list/RoundedListWithDividers.kt | 31 +++--- .../staking/GetStakingTransactionUseCase.kt | 4 +- .../presentation/state/StakingStateRouter.kt | 11 +- .../impl/presentation/state/StakingUiState.kt | 11 -- .../ui/StakingInitialInfoContent.kt | 69 +++++++----- .../ui/StakingNavigationButtons.kt | 10 +- .../impl/presentation/ui/StakingScreen.kt | 2 +- .../TokenDetailsLoadedBalanceConverter.kt | 104 ++++++++++++------ .../state/factory/TokenDetailsStateFactory.kt | 2 + .../viewmodels/TokenDetailsViewModel.kt | 13 ++- .../WalletCurrencyActionsClickIntents.kt | 3 + 18 files changed, 180 insertions(+), 99 deletions(-) rename app/src/androidTest/kotlin/com/tangem/tests/{ScanErrorTest.kt => ScanEmptyTest.kt} (100%) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ScanEmptyTest.kt similarity index 100% rename from app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt rename to app/src/androidTest/kotlin/com/tangem/tests/ScanEmptyTest.kt diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index df3279230b..500bfaa797 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -633,7 +633,7 @@ ≈ %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 - %1s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. + %1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. Ungültige Adresse %1$s (%2$s) Transaktion gesendet diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index a07a86a3ab..4326cc2b26 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -490,6 +490,8 @@ Il s\'agit du coût que vous êtes prêt à payer pour chaque unité de gaz. Plus le prix du gaz est élevé, plus votre transaction sera traitée rapidement. (Frais de priorité inclus) Frais de priorité Les frais qu\'un utilisateur peut payer aux mineurs ou aux validateurs pour accélérer l\'inclusion de leur transaction dans un bloc. + Les frais requis pour l\'utilisation de chaque sortie de transaction non dépensée (UTXO) dans le réseau Kaspa. Plus vous utilisez d’UTXO dans une transaction, plus les frais seront élevés. + KAS pour UTXO %1$s, %2$s Adresse Destination Tag @@ -718,6 +720,7 @@ Paramètres du portefeuille Tangem Utilisez %s ou scannez une carte pour déverrouiller l\'accès à votre portefeuille + Il semble que l\'activation de la carte ne se soit pas déroulée correctement. Cela peut être dû à un problème avec le module NFC de votre appareil ou à une mauvaise connexion de la carte sur votre appareil. Veuillez contacter notre équipe de support pour obtenir de l’aide. Erreur d\'activation Selon les développeurs du réseau BNB, le support de la norme BEP-2\nprendra fin en juin 2024. Pour éviter de perdre des actifs avec cette norme, veuillez les convertir à la norme BEP-20. Utilisez notre service de d\'échange pour les transférer sur le réseau BNB Smart Chain. BNB Beacon Chain va s\'arrêter de fonctionner diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index de7d3bc1e3..2c83a84083 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -621,7 +621,7 @@ ≈ %1$s (%2$s: 手数料を含む) %sが送信されます 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 - %1sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 + %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 無効なアドレス %1$s ( %2$s ) 取引が送信されました diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 9784dd7cc8..e83c0613ab 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -565,7 +565,7 @@ ≈ %1$s (вкл. комиссию: %2$s) %s будет отправлено Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время - %1s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес. + %1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес. Неверный адрес Транзакция отправлена Подготовьтесь к сканированию карты, которую вы хотите настроить. diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 57f722155b..4385ef8469 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -645,7 +645,7 @@ ≈ %1$s (вкл. комісію: %2$s ) %s буде надіслано Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час - %1s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron(TRX) на свій рахунок. + %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron(TRX) на свій рахунок. Недійсна адреса %1$s (%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 28f111ab8b..ae1e771935 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -4,6 +4,7 @@ 管理代幣 僅將 %1$s (%2$s) 從 %3$s 網絡發送到此地址。使用其他代幣和網絡可能會導致資金損失 請求支持 + 再試一次 此功能不在展示模式中提供 原因:%s 無法發送交易 @@ -36,6 +37,7 @@ 安全模式 卡片設置 接受 + 允許 注意 餘額: %s 餘額 @@ -127,6 +129,9 @@ 反饋 Tangem反饋 無法發送交易 + 數量 %s + 要繼續,您需要允許 %1s 智能合約使用您的 %2s + 賦予權限 掃描卡片 要更改訪問密碼,請完全按照上圖所示連接手機和卡片 要更改密碼,請完全按照上圖所示連接手機和卡 @@ -171,7 +176,6 @@ 這此情況,您必須要重新開始 您想要離開啟用程序嗎? 開始 - 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎? 創建備份 閱讀更多關於助記詞的訊息 @@ -288,7 +292,7 @@ 您即將在主屏幕上隱藏此代幣。您可以隨時通過管理代幣頁面將其添加回來。 隱藏 %s 隱藏代幣 - %1$s 代幣是 %2$s 網絡上的主要貨幣,只要列表中還有該網絡上的其他代幣,它就無法被隱藏。 + %1$s (%2$s) 代幣是 %3$s 網絡上的主要貨幣,只要列表中還有該網絡上的其他代幣,它就無法被隱藏。 無法隱藏 %s 您還沒有任何交易 無法加載交易 diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt index 8161f2ff69..1779adf487 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt @@ -4,6 +4,7 @@ import android.content.res.Configuration 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.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -20,19 +21,23 @@ import kotlinx.collections.immutable.persistentListOf @Composable fun RoundedListWithDividers(rows: List, modifier: Modifier = Modifier) { LazyColumn(modifier = modifier) { - itemsIndexed( - items = rows, - key = { _, item -> item.id }, - ) { index, row -> - InitialInfoContentRow( - startText = row.startText.resolveReference(), - endText = row.endText.resolveReference(), - cornersToRound = getCornersToRound(index, rows.size), - iconClick = row.iconClick, - ) - if (index < rows.lastIndex) { - RoundedListDivider() - } + this.roundedListItems(rows) + } +} + +fun LazyListScope.roundedListItems(rows: List) { + itemsIndexed( + items = rows, + key = { _, item -> item.id }, + ) { index, row -> + InitialInfoContentRow( + startText = row.startText.resolveReference(), + endText = row.endText.resolveReference(), + cornersToRound = getCornersToRound(index, rows.size), + iconClick = row.iconClick, + ) + if (index < rows.lastIndex) { + RoundedListDivider() } } } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt index 02abd643a4..d6d147b1c3 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt @@ -23,7 +23,9 @@ class GetStakingTransactionUseCase( // workaround, sometimes transaction is not created immediately after actions/enter delay(PATCH_TRANSACTION_REQUEST_DELAY) - val createdTransaction = createAction.transactions?.get(0) ?: error("No available transaction to patch") + val createdTransaction = createAction.transactions + ?.get(createAction.currentStepIndex) + ?: error("No available transaction to patch") val patchedTransaction = stakingRepository.constructTransaction(createdTransaction.id) patchedTransaction diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 2629f6405b..d0485b1812 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -32,10 +32,17 @@ internal class StakingStateRouter( } fun onPrevClick() { - when (stateController.uiState.value.currentStep) { + val uiState = stateController.uiState.value + when (uiState.currentStep) { StakingStep.InitialInfo -> onBackClick() StakingStep.Amount -> showInitial() - StakingStep.Confirmation -> showAmount() + StakingStep.Confirmation -> { + if (uiState.routeType == RouteType.OTHER || uiState.routeType == RouteType.UNSTAKE) { + showInitial() + } else { + showAmount() + } + } StakingStep.Validators -> showConfirmation() StakingStep.RewardsValidators -> showInitial() } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 2ad72821a5..ae02e3c71b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -55,17 +55,6 @@ internal sealed class StakingStates { val isStakeMoreAvailable: Boolean, ) : InitialInfoState() - data class InitialInfoItems( - val available: String, - val onStake: String, - val aprRange: TextReference, - val unbondingPeriod: String, - val minimumRequirement: String, - val rewardClaiming: String, - val warmupPeriod: String, - val rewardSchedule: String, - ) - data class Empty( override val isPrimaryButtonEnabled: Boolean = false, ) : InitialInfoState() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 961cdb9643..eae933e9b6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -1,12 +1,12 @@ package com.tangem.features.staking.impl.presentation.ui import android.content.res.Configuration -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.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon @@ -22,10 +22,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImageInfo -import com.tangem.core.ui.components.list.RoundedListWithDividers +import com.tangem.core.ui.components.list.roundedListItems import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -39,37 +40,53 @@ import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.extensions.orZero +private const val STAKING_REWARD_BLOCK_KEY = "StakingRewardBlock" +private const val METRICS_BLOCK_KEY = "MetricsBlock" +private const val ACTIVE_STAKING_BLOCK_KEY = "ActiveStakingBlock" + +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, clickIntents: StakingClickIntents) { if (state !is StakingStates.InitialInfoState.Data) return - Column( - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - modifier = Modifier // Do not put fillMaxSize() in here + LazyColumn( + modifier = Modifier .background(TangemTheme.colors.background.tertiary) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), + .fillMaxSize(), + contentPadding = PaddingValues(TangemTheme.dimens.spacing16), ) { - AnimatedVisibility(state.yieldBalance == InnerYieldBalanceState.Empty) { - MetricsBlock(state) - } - RoundedListWithDividers(state.infoItems) - AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") { - if (it is InnerYieldBalanceState.Data) { - StakingRewardBlock( - rewardCrypto = it.rewardsCrypto, - rewardFiat = it.rewardsFiat, - isRewardsToClaim = it.isRewardsToClaim, - onRewardsClick = clickIntents::openRewardsValidators, - ) + if (state.yieldBalance == InnerYieldBalanceState.Empty) { + item(key = METRICS_BLOCK_KEY) { + Column(modifier = Modifier.animateItemPlacement()) { + MetricsBlock(state) + SpacerH12() + } } } - AnimatedContent(targetState = state.yieldBalance, label = "Staking yield visibility animation") { - if (it is InnerYieldBalanceState.Data) { - ActiveStakingBlock(it.balance, clickIntents::onActiveStake) + + this.roundedListItems(state.infoItems) + item { SpacerH12() } + + if (state.yieldBalance is InnerYieldBalanceState.Data) { + item(key = STAKING_REWARD_BLOCK_KEY) { + Column(modifier = Modifier.animateItemPlacement()) { + StakingRewardBlock( + rewardCrypto = state.yieldBalance.rewardsCrypto, + rewardFiat = state.yieldBalance.rewardsFiat, + isRewardsToClaim = state.yieldBalance.isRewardsToClaim, + onRewardsClick = clickIntents::openRewardsValidators, + ) + SpacerH12() + } + } + } + + if (state.yieldBalance is InnerYieldBalanceState.Data) { + item(key = ACTIVE_STAKING_BLOCK_KEY) { + Column(modifier = Modifier.animateItemPlacement()) { + ActiveStakingBlock(state.yieldBalance.balance, clickIntents::onActiveStake) + SpacerH12() + } } } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt index 67a7000962..de810623ee 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -133,7 +133,12 @@ private fun getButtonData(currentState: StakingUiState): Int { if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { R.string.common_close } else { - R.string.common_stake + when (currentState.routeType) { + RouteType.STAKE -> R.string.common_stake + RouteType.CLAIM -> R.string.common_claim_rewards + RouteType.UNSTAKE -> R.string.common_unstake + RouteType.OTHER -> R.string.common_stake + } } } else { R.string.common_close @@ -182,7 +187,8 @@ private fun isButtonEnabled(uiState: StakingUiState): Pair { return when (uiState.currentStep) { StakingStep.InitialInfo -> { val initialState = uiState.initialInfoState as? StakingStates.InitialInfoState.Data - val isDisplayed = initialState?.isStakeMoreAvailable == true + val isDisplayed = initialState?.isStakeMoreAvailable == true || + initialState?.yieldBalance is InnerYieldBalanceState.Empty uiState.initialInfoState.isPrimaryButtonEnabled to isDisplayed } StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled to true diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index c26935b80c..c79fca59ff 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.withIndex @Composable internal fun StakingScreen(uiState: StakingUiState) { - BackHandler(onBack = uiState.clickIntents::onBackClick) + BackHandler(onBack = uiState.clickIntents::onPrevClick) Column( modifier = Modifier .background(color = TangemTheme.colors.background.tertiary) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index b5b0f8a5c6..6aa3923cd9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -11,10 +11,11 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType +import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState @@ -31,9 +32,11 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal +@Suppress("LongParameterList") internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, + private val stakingEntryInfoProvider: Provider, private val symbol: String, private val decimals: Int, private val clickIntents: TokenDetailsClickIntents, @@ -136,39 +139,28 @@ internal class TokenDetailsLoadedBalanceConverter( private fun getYieldBalance(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM { val yieldBalance = status.value.yieldBalance as? YieldBalance.Data - val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance() - val stakingRewardAmount = yieldBalance?.getRewardStakingBalance() - val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } - return if (stakingCryptoAmount.isNullOrZero()) { - StakingBlockUM.Loading(state.tokenInfoBlockState.iconState) - } else { - StakingBlockUM.Staked( - cryptoAmount = stakingCryptoAmount, - fiatAmount = stakingFiatAmount, - cryptoValue = stringReference( - BigDecimalFormatter.formatCryptoAmount(stakingCryptoAmount, symbol, decimals), - ), - fiatValue = stringReference( - BigDecimalFormatter.formatFiatAmount( - stakingFiatAmount, - appCurrencyProvider().code, - appCurrencyProvider().symbol, - ), - ), - rewardValue = resourceReference( - R.string.staking_details_rewards_to_claim, - wrappedList( - BigDecimalFormatter.formatFiatAmount( - stakingRewardAmount, - appCurrencyProvider().code, - appCurrencyProvider().symbol, - ), - ), - ), - onStakeClicked = clickIntents::onStakeBannerClick, - ) + val stakingEntryInfo = stakingEntryInfoProvider.invoke() + val iconState = state.tokenInfoBlockState.iconState + + return when { + stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> { + getStakeAvailableState(stakingEntryInfo, iconState) + } + stakingCryptoAmount.isNullOrZero() && stakingEntryInfo == null -> { + StakingBlockUM.Error(iconState = iconState) + } + else -> { + val stakingRewardAmount = yieldBalance?.getRewardStakingBalance() + val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } + + getStakedState( + stakingCryptoAmount = stakingCryptoAmount, + stakingFiatAmount = stakingFiatAmount, + stakingRewardAmount = stakingRewardAmount, + ) + } } } @@ -195,6 +187,54 @@ internal class TokenDetailsLoadedBalanceConverter( } } + private fun getStakeAvailableState( + stakingEntryInfo: StakingEntryInfo, + iconState: IconState, + ): StakingBlockUM.StakeAvailable { + return StakingBlockUM.StakeAvailable( + interestRate = BigDecimalFormatter.formatPercent( + percent = stakingEntryInfo.interestRate, + useAbsoluteValue = true, + ), + periodInDays = stakingEntryInfo.periodInDays, + tokenSymbol = stakingEntryInfo.tokenSymbol, + iconState = iconState, + onStakeClicked = clickIntents::onStakeBannerClick, + ) + } + + private fun getStakedState( + stakingCryptoAmount: BigDecimal?, + stakingFiatAmount: BigDecimal?, + stakingRewardAmount: BigDecimal?, + ): StakingBlockUM.Staked { + return StakingBlockUM.Staked( + cryptoAmount = stakingCryptoAmount, + fiatAmount = stakingFiatAmount, + cryptoValue = stringReference( + BigDecimalFormatter.formatCryptoAmount(stakingCryptoAmount, symbol, decimals), + ), + fiatValue = stringReference( + BigDecimalFormatter.formatFiatAmount( + stakingFiatAmount, + appCurrencyProvider().code, + appCurrencyProvider().symbol, + ), + ), + rewardValue = resourceReference( + R.string.staking_details_rewards_to_claim, + wrappedList( + BigDecimalFormatter.formatFiatAmount( + stakingRewardAmount, + appCurrencyProvider().code, + appCurrencyProvider().symbol, + ), + ), + ), + onStakeClicked = clickIntents::onStakeBannerClick, + ) + } + private fun CryptoCurrencyStatus.Value.toContentConfig(currencySymbol: String): MarketPriceBlockState.Content { return MarketPriceBlockState.Content( currencySymbol = currencySymbol, 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 3d6080faba..402e674bec 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 @@ -50,6 +50,7 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, + private val stakingEntryInfoProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, @@ -79,6 +80,7 @@ internal class TokenDetailsStateFactory( TokenDetailsLoadedBalanceConverter( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, + stakingEntryInfoProvider = stakingEntryInfoProvider, symbol = symbol, decimals = decimals, clickIntents = clickIntents, 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 b885be8766..0e56efee0d 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 @@ -34,6 +34,7 @@ import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo @@ -143,15 +144,16 @@ internal class TokenDetailsViewModel @Inject constructor( private val refreshStateJobHolder = JobHolder() private val warningsJobHolder = JobHolder() private val swapTxJobHolder = JobHolder() - private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null - - private var swapTxStatusTaskScheduler = SingleTaskScheduler>() - private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null + private var stakingEntryInfo: StakingEntryInfo? = null + private var swapTxStatusTaskScheduler = SingleTaskScheduler>() + private val stateFactory = TokenDetailsStateFactory( currentStateProvider = Provider { uiState.value }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + stakingEntryInfoProvider = Provider { stakingEntryInfo }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, clickIntents = this, symbol = cryptoCurrency.symbol, @@ -397,7 +399,8 @@ internal class TokenDetailsViewModel @Inject constructor( cryptoCurrencyId = cryptoCurrency.id, symbol = cryptoCurrency.symbol, ) - internalUiState.value = stateFactory.getStateWithStaking(stakingInfo) + + stakingEntryInfo = stakingInfo.getOrNull() } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index c6b5eb1cff..bdf5dae4ba 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -370,6 +370,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId)) + viewModelScope.launch { val userWalletId = stateHolder.getSelectedWalletId() val cryptoCurrency = cryptoCurrencyStatus.currency From eeec58e67654ab09716e14d5bce81319c824a628 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jul 2024 14:14:28 +0300 Subject: [PATCH 05/87] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 7 ++ .../tangem/common/ui/charts/MarketChart.kt | 66 +++++++------- .../markets/DefaultMarketsTokenRepository.kt | 10 +++ .../data/markets/MarketsBatchUpdateFetcher.kt | 4 +- ...artConverter.kt => TokenChartConverter.kt} | 4 +- .../converters/TokenMarketChartsConverter.kt | 8 +- .../com/tangem/domain/markets/TokenChart.kt | 4 +- .../markets/GetMarketsTokenListFlowUseCase.kt | 1 + .../markets/GetTokenPriceChartUseCase.kt | 15 ++++ .../repositories/MarketsTokenRepository.kt | 2 + ...tComponent.kt => MarketsEntryComponent.kt} | 4 +- features/markets/impl/build.gradle.kts | 1 + .../markets/DefaultMarketsEntryComponent.kt | 85 +++++++++++++++++++ .../markets/MarketsEntryChildFactory.kt | 45 ++++++++++ .../api/MarketsTokenDetailsComponent.kt | 15 ++++ .../details/api/TokenMarketSerializable.kt | 40 +++++++++ .../DefaultMarketsTokenDetailsComponent.kt | 43 ++++++++++ .../details/impl/di/ComponentModule.kt | 20 +++++ .../markets/details/impl/di/ModelModule.kt | 20 +++++ .../impl/model/MarketsTokenDetailsModel.kt | 60 +++++++++++++ .../impl/ui/MarketsTokenDetailsContent.kt | 31 +++++++ .../ui/components/MarketTokenDetailsChart.kt | 29 +++++++ .../impl/ui/entity/MarketsTokenDetailsUM.kt | 20 +++++ .../features/markets/di/ComponentModule.kt | 6 +- .../api/MarketsTokenListComponent.kt | 25 ++++++ .../impl/DefaultMarketsTokenListComponent.kt} | 31 +++++-- .../tokenlist/impl/di/ComponentModule.kt | 20 +++++ .../{ => tokenlist/impl}/di/ModelModule.kt | 4 +- .../impl}/model/MarketsListModel.kt | 30 +++++-- .../converters/MarketsTokenItemConverter.kt | 8 +- .../MarketsListBatchFlowManager.kt | 20 +++-- .../statemanager/MarketsListUMStateManager.kt | 14 +-- .../impl}/model/utils/LoggingUtils.kt | 2 +- .../{ => tokenlist/impl}/ui/MarketsList.kt | 17 ++-- .../impl}/ui/components/MarketsListItem.kt | 6 +- .../components/MarketsListItemPlaceholder.kt | 2 +- .../ui/components/MarketsListLazyColumn.kt | 9 +- .../MarketsListSortByBottomSheet.kt | 6 +- .../impl}/ui/components/UnableToLoadData.kt | 2 +- .../impl}/ui/entity/MarketsListItemUM.kt | 2 +- .../impl}/ui/entity/MarketsListUM.kt | 3 +- .../ui/entity/SortByBottomSheetContentUM.kt | 2 +- .../MarketChartListItemPreviewDataProvider.kt | 4 +- .../wallet/presentation/WalletFragment.kt | 10 +-- .../router/DefaultWalletRouter.kt | 6 +- .../presentation/router/InnerWalletRouter.kt | 4 +- .../presentation/wallet/ui/WalletScreen.kt | 14 +-- 47 files changed, 658 insertions(+), 123 deletions(-) rename data/markets/src/main/java/com/tangem/data/markets/converters/{TokenListChartConverter.kt => TokenChartConverter.kt} (84%) create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt rename features/markets/api/src/main/kotlin/com/tangem/features/markets/component/{MarketsListComponent.kt => MarketsEntryComponent.kt} (82%) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/MarketsEntryChildFactory.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/MarketsTokenDetailsComponent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/TokenMarketSerializable.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{component/impl/DefaultMarketsListComponent.kt => tokenlist/impl/DefaultMarketsTokenListComponent.kt} (55%) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/di/ComponentModule.kt rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/di/ModelModule.kt (78%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/model/MarketsListModel.kt (90%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/model/converters/MarketsTokenItemConverter.kt (94%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/model/statemanager/MarketsListBatchFlowManager.kt (93%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/model/statemanager/MarketsListUMStateManager.kt (91%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/model/utils/LoggingUtils.kt (96%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/ui/MarketsList.kt (93%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/ui/components/MarketsListItem.kt (98%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/ui/components/MarketsListItemPlaceholder.kt (98%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/ui/components/MarketsListLazyColumn.kt (95%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/ui/components/MarketsListSortByBottomSheet.kt (93%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/ui/components/UnableToLoadData.kt (96%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/ui/entity/MarketsListItemUM.kt (94%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/ui/entity/MarketsListUM.kt (94%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/ui/entity/SortByBottomSheetContentUM.kt (79%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{ => tokenlist/impl}/ui/preview/MarketChartListItemPreviewDataProvider.kt (96%) diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 765e3bcb7f..f07561bf15 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.GetTokenPriceChartUseCase import com.tangem.domain.markets.repositories.MarketsTokenRepository import dagger.Module import dagger.Provides @@ -19,4 +20,10 @@ object MarketsDomainModule { ): GetMarketsTokenListFlowUseCase { return GetMarketsTokenListFlowUseCase(marketsTokenRepository = marketsTokenRepository) } + + @Provides + @Singleton + fun provideGetTokenPriceChartUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenPriceChartUseCase { + return GetTokenPriceChartUseCase(marketsTokenRepository = marketsTokenRepository) + } } \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt index 9bbaeaa48b..58c95c1451 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -70,9 +70,9 @@ private const val GUIDELINES_COUNT = 3 fun MarketChart( modifier: Modifier = Modifier, state: MarketChartState = rememberMarketChartState(), - splitChartSegmentColor: Color, - @FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float, - @FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float, + splitChartSegmentColor: Color = TangemTheme.colors.icon.inactive, + @FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float = 0.24f, + @FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float = 0.24f, noChartContent: @Composable BoxScope.() -> Unit, ) { var canvasWidth by remember { mutableIntStateOf(0) } @@ -358,41 +358,47 @@ private fun MarketChartPreview( text = "Change marker highlight side", ) } - Button(onClick = { - coroutineScope.launch { - dataProducer.runTransactionSuspend { - updateData { - MarketChartData.Data( - x = it.x, - y = it.y.reversed(), - ) + Button( + onClick = { + coroutineScope.launch { + dataProducer.runTransactionSuspend { + updateData { + MarketChartData.Data( + x = it.x, + y = it.y.reversed(), + ) + } } } - } - },) { + }, + ) { Text("Change Data") } - Button(onClick = { - dataProducer.runTransaction { - updateLook { it.copy(animationOnDataChange = it.animationOnDataChange.not()) } - } - },) { + Button( + onClick = { + dataProducer.runTransaction { + updateLook { it.copy(animationOnDataChange = it.animationOnDataChange.not()) } + } + }, + ) { Text("Change animationOnDataChange = ${look.animationOnDataChange}") } - Button(onClick = { - dataProducer.runTransaction { - updateLook { - it.copy( - type = if (it.type == MarketChartLook.Type.Growing) { - MarketChartLook.Type.Falling - } else { - MarketChartLook.Type.Growing - }, - ) + Button( + onClick = { + dataProducer.runTransaction { + updateLook { + it.copy( + type = if (it.type == MarketChartLook.Type.Growing) { + MarketChartLook.Type.Falling + } else { + MarketChartLook.Type.Growing + }, + ) + } } - } - },) { + }, + ) { Text("Change color type") } } diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index be83e51fa2..eda05fed37 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -19,6 +19,7 @@ internal class DefaultMarketsTokenRepository( ) : MarketsTokenRepository { private val tokenListConverter = TokenMarketListConverter() + private val tokenChartConverter = TokenChartConverter() private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher( prefetchDistance = firstBatchSize, @@ -89,4 +90,13 @@ internal class DefaultMarketsTokenRepository( updateFetcher = tokenMarketsUpdateFetcher, ).toBatchFlow() } + + override suspend fun getChart(interval: PriceChangeInterval, tokenId: String): TokenChart { + val response = marketsApi.getCoinChart( + currency = tokenId, + interval = interval.toRequestParam(), + ) + + return tokenChartConverter.convert(interval, response.getOrThrow()) + } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt index 625f90797d..0da0fcd457 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt @@ -1,6 +1,6 @@ package com.tangem.data.markets -import com.tangem.data.markets.converters.TokenListChartConverter +import com.tangem.data.markets.converters.TokenChartConverter import com.tangem.data.markets.converters.TokenMarketChartsConverter import com.tangem.data.markets.converters.TokenQuotesConverter import com.tangem.data.markets.converters.toRequestParam @@ -21,7 +21,7 @@ internal class MarketsBatchUpdateFetcher( private val tangemTechApi: TangemTechApi, ) : BatchUpdateFetcher, TokenMarketUpdateRequest> { - private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter()) + private val tokenListChartsConverter = TokenMarketChartsConverter(TokenChartConverter()) private val tokenQuotesConverter = TokenQuotesConverter() override suspend fun BatchUpdateFetcher.UpdateContext>.fetchUpdateAsync( diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenChartConverter.kt similarity index 84% rename from data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt rename to data/markets/src/main/java/com/tangem/data/markets/converters/TokenChartConverter.kt index 0b4cf678c6..bdd90f2733 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenChartConverter.kt @@ -4,13 +4,13 @@ import com.tangem.datasource.api.markets.models.response.TokenMarketChartRespons import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenChart -class TokenListChartConverter { +class TokenChartConverter { fun convert(interval: PriceChangeInterval, value: TokenMarketChartResponse): TokenChart { return TokenChart( interval = interval, priceY = value.prices.values.toList(), - timeStamp = value.prices.keys.toList(), + timeStamps = value.prices.keys.toList(), ) } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt index dcfdee4638..4eb118ab29 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt @@ -5,7 +5,7 @@ import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarket class TokenMarketChartsConverter( - private val tokenListChartConverter: TokenListChartConverter, + private val tokenChartConverter: TokenChartConverter, ) { fun convert( @@ -19,13 +19,13 @@ class TokenMarketChartsConverter( } return when (interval) { PriceChangeInterval.H24 -> chartsToCopy.copy( - h24 = tokenListChartConverter.convert(interval, prices), + h24 = tokenChartConverter.convert(interval, prices), ) PriceChangeInterval.WEEK -> chartsToCopy.copy( - week = tokenListChartConverter.convert(interval, prices), + week = tokenChartConverter.convert(interval, prices), ) PriceChangeInterval.MONTH -> chartsToCopy.copy( - month = tokenListChartConverter.convert(interval, prices), + month = tokenChartConverter.convert(interval, prices), ) else -> error("unsupported interval=$interval. This shouldn't have happened.") } diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt index d239ae395a..4e23077cf1 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt @@ -5,9 +5,9 @@ import java.math.BigDecimal data class TokenChart( val interval: PriceChangeInterval, val priceY: List, - val timeStamp: List, + val timeStamps: List, ) { init { - require(priceY.size == timeStamp.size) + require(priceY.size == timeStamps.size) } } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetMarketsTokenListFlowUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetMarketsTokenListFlowUseCase.kt index 8ba13797a3..c182f7bd43 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetMarketsTokenListFlowUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetMarketsTokenListFlowUseCase.kt @@ -16,6 +16,7 @@ class GetMarketsTokenListFlowUseCase( firstBatchSize = batchFlowType.firstBatchSize, nextBatchSize = batchFlowType.nextBatchSize, ) + // TODO listen quotes updates flow and update them in other parts of the application } enum class BatchFlowType( diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt new file mode 100644 index 0000000000..e2db0be0d3 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.markets + +import arrow.core.Either +import com.tangem.domain.markets.repositories.MarketsTokenRepository + +class GetTokenPriceChartUseCase( + private val marketsTokenRepository: MarketsTokenRepository, +) { + + suspend operator fun invoke(interval: PriceChangeInterval, tokenId: String): Either { + return Either.catch { + marketsTokenRepository.getChart(interval = interval, tokenId = tokenId) + }.mapLeft {} + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index 31693705dc..d6cb5a094b 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -9,4 +9,6 @@ interface MarketsTokenRepository { firstBatchSize: Int, nextBatchSize: Int, ): TokenListBatchFlow + + suspend fun getChart(interval: PriceChangeInterval, tokenId: String): TokenChart } \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsListComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsEntryComponent.kt similarity index 82% rename from features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsListComponent.kt rename to features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsEntryComponent.kt index c0d14dbea5..21ec5c5772 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsListComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsEntryComponent.kt @@ -8,7 +8,7 @@ import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.context.AppComponentContext @Stable -interface MarketsListComponent { +interface MarketsEntryComponent { @Composable fun BottomSheetContent( @@ -18,6 +18,6 @@ interface MarketsListComponent { ) interface Factory { - fun create(context: AppComponentContext): MarketsListComponent + fun create(context: AppComponentContext): MarketsEntryComponent } } \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 46bc8c674a..d5b82bb93d 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) implementation(deps.timber) + implementation(deps.decompose.ext.compose) /* Core */ implementation(projects.core.decompose) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt new file mode 100644 index 0000000000..987d11c0a8 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt @@ -0,0 +1,85 @@ +package com.tangem.features.markets + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.push +import com.arkivanov.decompose.value.Value +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.domain.markets.TokenMarket +import com.tangem.features.markets.component.BottomSheetState +import com.tangem.features.markets.component.MarketsEntryComponent +import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent +import com.tangem.features.markets.details.api.toSerializable +import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent +import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultMarketsEntryComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + private val marketsEntryChildFactory: MarketsEntryChildFactory, +) : MarketsEntryComponent, AppComponentContext by context { + + private val model: MarketsListModel = getOrCreateModel() + + private val stackNavigation = StackNavigation() + + val stack: Value> = childStack( + key = "main", + source = stackNavigation, + serializer = MarketsEntryChildFactory.Child.serializer(), + initialConfiguration = MarketsEntryChildFactory.Child.TokenList, + handleBackButton = true, + childFactory = { configuration, componentContext -> + marketsEntryChildFactory.createChild( + child = configuration, + appComponentContext = childByContext(componentContext), + onTokenSelected = ::marketsListTokenSelected, + ) + }, + ) + + @Composable + override fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) { + Children(stack) { + when (it.configuration) { + is MarketsEntryChildFactory.Child.TokenDetails -> { + (it.instance as MarketsTokenDetailsComponent).Content(modifier) + } + MarketsEntryChildFactory.Child.TokenList -> { + (it.instance as MarketsTokenListComponent).BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) + } + } + } + } + + private fun marketsListTokenSelected(token: TokenMarket) { + stackNavigation.push( + configuration = MarketsEntryChildFactory.Child.TokenDetails( + params = MarketsTokenDetailsComponent.Params(token.toSerializable()), + ), + ) + } + + @AssistedFactory + interface Factory : MarketsEntryComponent.Factory { + override fun create(context: AppComponentContext): DefaultMarketsEntryComponent + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/MarketsEntryChildFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/MarketsEntryChildFactory.kt new file mode 100644 index 0000000000..9ad07ec122 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/MarketsEntryChildFactory.kt @@ -0,0 +1,45 @@ +package com.tangem.features.markets + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.markets.TokenMarket +import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent +import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent +import kotlinx.serialization.Serializable +import javax.inject.Inject + +internal class MarketsEntryChildFactory @Inject constructor( + private val tokenListComponentFactory: MarketsTokenListComponent.Factory, + private val tokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, +) { + + @Serializable + sealed interface Child { + + @Serializable + data object TokenList : Child + + @Serializable + data class TokenDetails(val params: MarketsTokenDetailsComponent.Params) : Child + } + + fun createChild( + child: Child, + appComponentContext: AppComponentContext, + onTokenSelected: (TokenMarket) -> Unit, + ): Any { + return when (child) { + is Child.TokenDetails -> { + tokenDetailsComponentFactory.create( + context = appComponentContext, + params = child.params, + ) + } + is Child.TokenList -> { + tokenListComponentFactory.create( + context = appComponentContext, + onTokenSelected = onTokenSelected, + ) + } + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/MarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/MarketsTokenDetailsComponent.kt new file mode 100644 index 0000000000..74407f6910 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/MarketsTokenDetailsComponent.kt @@ -0,0 +1,15 @@ +package com.tangem.features.markets.details.api + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import kotlinx.serialization.Serializable + +@Stable +interface MarketsTokenDetailsComponent : ComposableContentComponent { + + @Serializable + data class Params(val token: TokenMarketSerializable) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/TokenMarketSerializable.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/TokenMarketSerializable.kt new file mode 100644 index 0000000000..4e978ed5ee --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/TokenMarketSerializable.kt @@ -0,0 +1,40 @@ +package com.tangem.features.markets.details.api + +import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.markets.TokenMarket +import kotlinx.serialization.Serializable + +@Serializable +data class TokenMarketSerializable( + val id: String, + val name: String, + val symbol: String, + val marketCap: SerializedBigDecimal?, + val tokenQuotes: Quotes, + val imageUrl: String, +) { + + @Serializable + data class Quotes( + val currentPrice: SerializedBigDecimal, + val h24Percent: SerializedBigDecimal, + val weekPercent: SerializedBigDecimal, + val monthPercent: SerializedBigDecimal, + ) +} + +fun TokenMarket.toSerializable(): TokenMarketSerializable { + return TokenMarketSerializable( + id = id, + name = name, + symbol = symbol, + marketCap = marketCap, + tokenQuotes = TokenMarketSerializable.Quotes( + currentPrice = tokenQuotes.currentPrice, + h24Percent = tokenQuotes.h24Percent(), + weekPercent = tokenQuotes.weekPercent(), + monthPercent = tokenQuotes.monthPercent(), + ), + imageUrl = imageUrlLarge, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt new file mode 100644 index 0000000000..ad994d8f70 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -0,0 +1,43 @@ +package com.tangem.features.markets.details.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent +import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel +import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: MarketsTokenDetailsComponent.Params, +) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent { + + private val model: MarketsTokenDetailsModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + MarketsTokenDetailsContent( + state = state, + onBackClick = {}, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : MarketsTokenDetailsComponent.Factory { + override fun create( + context: AppComponentContext, + params: MarketsTokenDetailsComponent.Params, + ): DefaultMarketsTokenDetailsComponent + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..eb07f7d3b3 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.details.impl.di + +import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent +import com.tangem.features.markets.details.impl.DefaultMarketsTokenDetailsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindMarketsTokenDetailsComponent( + factory: DefaultMarketsTokenDetailsComponent.Factory, + ): MarketsTokenDetailsComponent.Factory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt new file mode 100644 index 0000000000..83e26ff055 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.details.impl.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(MarketsTokenDetailsModel::class) + fun provideMarketsTokenDetailsModel(model: MarketsTokenDetailsModel): Model +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt new file mode 100644 index 0000000000..e2bece6eca --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -0,0 +1,60 @@ +package com.tangem.features.markets.details.impl.model + +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.MarketChartDataProducer +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.domain.markets.GetTokenPriceChartUseCase +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent +import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import java.math.BigDecimal +import javax.inject.Inject + +internal class MarketsTokenDetailsModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, +) : Model() { + + val params = paramsContainer.require() + + private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) { + chartData = MarketChartData.NoData.Loading + } + + val state = MutableStateFlow( + MarketsTokenDetailsUM( + tokenName = params.token.name, + dateTimeText = "datetime", + priceChangePercentText = params.token.tokenQuotes.h24Percent.toString(), + priceChangeType = if (params.token.tokenQuotes.h24Percent < BigDecimal.ZERO) { + PriceChangeType.DOWN + } else { + PriceChangeType.UP + }, + chartState = MarketsTokenDetailsUM.ChartState( + dataProducer = chartDataProducer, + chartLook = MarketChartLook(), + onLoadRetryClick = {}, + ), + ), + ) + + init { + modelScope.launch { + val chart = getTokenPriceChartUseCase.invoke(PriceChangeInterval.H24, params.token.id) + + chart.onRight { + chartDataProducer.runTransactionSuspend { + chartData = MarketChartData.Data() + } + } + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt new file mode 100644 index 0000000000..9f091059d5 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -0,0 +1,31 @@ +package com.tangem.features.markets.details.impl.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM + +@Suppress("UnusedPrivateMember") +@Composable +internal fun MarketsTokenDetailsContent( + state: MarketsTokenDetailsUM, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + // TODO +} + +@Suppress("UnusedPrivateMember") +@Composable +private fun Content(state: MarketsTokenDetailsUM, onBackClick: () -> Unit, modifier: Modifier = Modifier) { + Column { + TangemTopAppBar( + title = state.tokenName, + startButton = TopAppBarButtonUM.Back(onBackClick), + ) + } + + // TODO +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt new file mode 100644 index 0000000000..8a92bdd1f3 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt @@ -0,0 +1,29 @@ +package com.tangem.features.markets.details.impl.ui.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.tangem.common.ui.charts.MarketChart +import com.tangem.common.ui.charts.state.rememberMarketChartState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM +import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData + +@Composable +fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) { + val chartState = rememberMarketChartState(state.dataProducer) + + MarketChart( + modifier = modifier, + state = chartState, + noChartContent = { + UnableToLoadData( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12) + .align(Alignment.Center), + onRetryClick = state.onLoadRetryClick, + ) + }, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt new file mode 100644 index 0000000000..2599cd5f21 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import com.tangem.common.ui.charts.state.MarketChartDataProducer +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.core.ui.components.marketprice.PriceChangeType + +data class MarketsTokenDetailsUM( + val tokenName: String, + val dateTimeText: String, + val priceChangePercentText: String, + val priceChangeType: PriceChangeType, + val chartState: ChartState, +) { + + data class ChartState( + val dataProducer: MarketChartDataProducer, + val chartLook: MarketChartLook, + val onLoadRetryClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt index f88b0b5ad0..c48bcbd802 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt @@ -1,7 +1,7 @@ package com.tangem.features.markets.di -import com.tangem.features.markets.component.MarketsListComponent -import com.tangem.features.markets.component.impl.DefaultMarketsListComponent +import com.tangem.features.markets.component.MarketsEntryComponent +import com.tangem.features.markets.DefaultMarketsEntryComponent import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -14,5 +14,5 @@ internal interface ComponentModule { @Binds @Singleton - fun bindMarketsListComponent(factory: DefaultMarketsListComponent.Factory): MarketsListComponent.Factory + fun bindMarketsListComponent(factory: DefaultMarketsEntryComponent.Factory): MarketsEntryComponent.Factory } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt new file mode 100644 index 0000000000..849eaf46fd --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt @@ -0,0 +1,25 @@ +package com.tangem.features.markets.tokenlist.api + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.markets.TokenMarket +import com.tangem.features.markets.component.BottomSheetState + +@Stable +interface MarketsTokenListComponent { + + @Composable + fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) + + interface Factory { + fun create(context: AppComponentContext, onTokenSelected: (TokenMarket) -> Unit): MarketsTokenListComponent + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt similarity index 55% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt index c5329d71a3..783559bd3c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.component.impl +package com.tangem.features.markets.tokenlist.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -9,20 +9,30 @@ import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.component.BottomSheetState -import com.tangem.features.markets.component.MarketsListComponent -import com.tangem.features.markets.model.MarketsListModel -import com.tangem.features.markets.ui.MarketsList +import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent +import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel +import com.tangem.features.markets.tokenlist.impl.ui.MarketsList import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach -internal class DefaultMarketsListComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, -) : MarketsListComponent, AppComponentContext by context { +class DefaultMarketsTokenListComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val onTokenSelected: (TokenMarket) -> Unit, +) : AppComponentContext by appComponentContext, MarketsTokenListComponent { private val model: MarketsListModel = getOrCreateModel() + init { + model.tokenSelected + .onEach { onTokenSelected(it) } + .launchIn(componentScope) + } + @Composable override fun BottomSheetContent( bottomSheetState: State, @@ -45,7 +55,10 @@ internal class DefaultMarketsListComponent @AssistedInject constructor( } @AssistedFactory - interface Factory : MarketsListComponent.Factory { - override fun create(context: AppComponentContext): DefaultMarketsListComponent + interface Factory : MarketsTokenListComponent.Factory { + override fun create( + context: AppComponentContext, + onTokenSelected: (TokenMarket) -> Unit, + ): DefaultMarketsTokenListComponent } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..bb44e19164 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/di/ComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.tokenlist.impl.di + +import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent +import com.tangem.features.markets.tokenlist.impl.DefaultMarketsTokenListComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindMarketsTokenListComponent( + factory: DefaultMarketsTokenListComponent.Factory, + ): MarketsTokenListComponent.Factory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/di/ModelModule.kt similarity index 78% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ModelModule.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/di/ModelModule.kt index 9ee24735da..17aa3f4cd5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ModelModule.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/di/ModelModule.kt @@ -1,8 +1,8 @@ -package com.tangem.features.markets.di +package com.tangem.features.markets.tokenlist.impl.di import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.decompose.model.Model -import com.tangem.features.markets.model.MarketsListModel +import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt similarity index 90% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index 97fc37c161..06fe406bca 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.model +package com.tangem.features.markets.tokenlist.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse @@ -7,11 +7,13 @@ import com.tangem.core.decompose.model.Model import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.component.BottomSheetState -import com.tangem.features.markets.model.statemanager.MarketsListUMStateManager -import com.tangem.features.markets.model.statemanager.MarketsListBatchFlowManager -import com.tangem.features.markets.ui.entity.ListUM -import com.tangem.features.markets.ui.entity.SortByTypeUM +import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager +import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager +import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -32,6 +34,8 @@ internal class MarketsListModel @Inject constructor( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, ) : Model() { + private var updateQuotesJob = JobHolder() + private val currentAppCurrency = getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } @@ -47,8 +51,8 @@ internal class MarketsListModel @Inject constructor( onLoadMoreUiItems = { activeListManager.loadMore() }, visibleItemsChanged = { visibleItemIds.value = it }, onRetryButtonClicked = { activeListManager.reload() }, + onTokenClick = { onTokenUIClicked(it) }, ) - private val mainMarketsListManager = MarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, @@ -59,6 +63,7 @@ internal class MarketsListModel @Inject constructor( modelScope = modelScope, dispatchers = dispatchers, ) + private val searchMarketsListManager = MarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, @@ -72,6 +77,10 @@ internal class MarketsListModel @Inject constructor( private var activeListManager: MarketsListBatchFlowManager = mainMarketsListManager + private val _tokenSelected = MutableSharedFlow() + + val tokenSelected = _tokenSelected.asSharedFlow() + val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED) val state = marketsListUMStateManager.state.asStateFlow() @@ -169,6 +178,7 @@ internal class MarketsListModel @Inject constructor( } .distinctUntilChanged() .collectLatest { visibleBatchKeys -> + // TODO load batch on scroll heat area activeListManager.loadCharts(visibleBatchKeys, marketsListUMStateManager.selectedInterval) } } @@ -210,7 +220,13 @@ internal class MarketsListModel @Inject constructor( mainMarketsListManager.reload() } - private var updateQuotesJob = JobHolder() + private fun onTokenUIClicked(token: MarketsListItemUM) { + modelScope.launch { + activeListManager.getTokenById(token.id)?.let { found -> + _tokenSelected.emit(found) + } + } + } private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) { launch { while (true) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt similarity index 94% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/converters/MarketsTokenItemConverter.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index c2badcd569..17050e129d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.model.converters +package com.tangem.features.markets.tokenlist.impl.model.converters import com.tangem.common.ui.charts.state.DefaultPointValuesConverter import com.tangem.common.ui.charts.state.MarketChartData @@ -7,8 +7,8 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket -import com.tangem.features.markets.ui.entity.MarketsListItemUM -import com.tangem.features.markets.ui.entity.MarketsListUM.TrendInterval +import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM.TrendInterval import com.tangem.utils.converter.Converter import java.math.BigDecimal import java.math.RoundingMode @@ -110,7 +110,7 @@ internal class MarketsTokenItemConverter( DefaultPointValuesConverter.convert( MarketChartData.Data( y = ct.priceY, - x = ct.timeStamp.map { it.toBigDecimal() }, + x = ct.timeStamps.map { it.toBigDecimal() }, ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListBatchFlowManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt similarity index 93% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListBatchFlowManager.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt index ae897a5b9a..42a790727c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListBatchFlowManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt @@ -1,14 +1,14 @@ -package com.tangem.features.markets.model.statemanager +package com.tangem.features.markets.tokenlist.impl.model.statemanager import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.* -import com.tangem.features.markets.model.converters.MarketsTokenItemConverter -import com.tangem.features.markets.model.utils.logAction -import com.tangem.features.markets.model.utils.logStatus -import com.tangem.features.markets.model.utils.logUpdateResults -import com.tangem.features.markets.ui.entity.MarketsListItemUM -import com.tangem.features.markets.ui.entity.MarketsListUM.TrendInterval -import com.tangem.features.markets.ui.entity.SortByTypeUM +import com.tangem.features.markets.tokenlist.impl.model.converters.MarketsTokenItemConverter +import com.tangem.features.markets.tokenlist.impl.model.utils.logAction +import com.tangem.features.markets.tokenlist.impl.model.utils.logStatus +import com.tangem.features.markets.tokenlist.impl.model.utils.logUpdateResults +import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM.TrendInterval +import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM import com.tangem.pagination.* import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -281,6 +281,10 @@ internal class MarketsListBatchFlowManager( .toSet() } + fun getTokenById(id: String): TokenMarket? { + return batchFlow.state.value.data.map { it.data }.flatten().find { it.id == id } + } + private fun SortByTypeUM.toRequestOrder(): TokenMarketListConfig.Order { return when (this) { SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt similarity index 91% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUMStateManager.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index d845850f44..0e2d8c4ac0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.model.statemanager +package com.tangem.features.markets.tokenlist.impl.model.statemanager import androidx.compose.runtime.Stable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig @@ -7,11 +7,11 @@ import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.markets.impl.R -import com.tangem.features.markets.ui.entity.SortByBottomSheetContentUM -import com.tangem.features.markets.ui.entity.ListUM -import com.tangem.features.markets.ui.entity.MarketsListItemUM -import com.tangem.features.markets.ui.entity.MarketsListUM -import com.tangem.features.markets.ui.entity.SortByTypeUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByBottomSheetContentUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.* @@ -20,6 +20,7 @@ internal class MarketsListUMStateManager( private val onLoadMoreUiItems: () -> Unit, private val visibleItemsChanged: (itemsKeys: List) -> Unit, private val onRetryButtonClicked: () -> Unit, + private val onTokenClick: (MarketsListItemUM) -> Unit, ) { private var sortByBottomSheetIsShown @@ -106,6 +107,7 @@ internal class MarketsListUMStateManager( showUnder100kTokens = true, onShowTokensUnder100kClicked = { }, triggerScrollReset = consumedEvent(), + onItemClick = onTokenClick, ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/utils/LoggingUtils.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/utils/LoggingUtils.kt similarity index 96% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/utils/LoggingUtils.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/utils/LoggingUtils.kt index e9cc0e8b31..8b612f6db5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/utils/LoggingUtils.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/utils/LoggingUtils.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.model.utils +package com.tangem.features.markets.tokenlist.impl.model.utils import com.tangem.domain.markets.TokenMarket import com.tangem.domain.markets.TokenMarketListConfig diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt similarity index 93% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 3ff1f1d4e7..72f70d8971 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.ui +package com.tangem.features.markets.tokenlist.impl.ui import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility @@ -36,13 +36,13 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.component.BottomSheetState import com.tangem.features.markets.impl.R -import com.tangem.features.markets.ui.components.MarketsListLazyColumn -import com.tangem.features.markets.ui.components.MarketsListSortByBottomSheet -import com.tangem.features.markets.ui.entity.ListUM -import com.tangem.features.markets.ui.entity.MarketsListUM -import com.tangem.features.markets.ui.entity.SortByBottomSheetContentUM -import com.tangem.features.markets.ui.entity.SortByTypeUM -import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider +import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn +import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet +import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByBottomSheetContentUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM +import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -242,6 +242,7 @@ private fun Preview() { visibleIdsChanged = {}, onShowTokensUnder100kClicked = {}, triggerScrollReset = consumedEvent(), + onItemClick = {}, ), searchBar = SearchBarUM( placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt similarity index 98% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt index 847f063309..e68c508ef5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.ui.components +package com.tangem.features.markets.tokenlist.impl.ui.components import android.content.res.Configuration import androidx.compose.animation.Animatable @@ -45,8 +45,8 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.windowsize.WindowSizeType import com.tangem.features.markets.impl.R -import com.tangem.features.markets.ui.entity.MarketsListItemUM -import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider +import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider import com.tangem.utils.StringsSigns.MINUS import kotlinx.coroutines.launch import kotlin.math.roundToInt diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItemPlaceholder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItemPlaceholder.kt similarity index 98% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItemPlaceholder.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItemPlaceholder.kt index 251844c2d6..fa7b373811 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItemPlaceholder.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItemPlaceholder.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.ui.components +package com.tangem.features.markets.tokenlist.impl.ui.components import android.content.res.Configuration import androidx.compose.foundation.background diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt similarity index 95% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListLazyColumn.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index dd7753fb72..56c53b9a72 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.ui.components +package com.tangem.features.markets.tokenlist.impl.ui.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -21,7 +21,7 @@ import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.impl.R -import com.tangem.features.markets.ui.entity.ListUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM import kotlinx.coroutines.launch private const val LOAD_NEXT_PAGE_ON_END_INDEX = 50 @@ -91,7 +91,10 @@ internal fun MarketsListLazyColumn( items = state.items, key = { it.id }, ) { item -> - MarketsListItem(model = item) + MarketsListItem( + model = item, + onClick = { state.onItemClick(item) }, + ) } if (isInSearchMode && state.showUnder100kTokens.not()) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListSortByBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt similarity index 93% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListSortByBottomSheet.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt index 4ee563d08c..67fe12a9c3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListSortByBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.ui.components +package com.tangem.features.markets.tokenlist.impl.ui.components import android.content.res.Configuration import androidx.compose.foundation.background @@ -19,8 +19,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.impl.R -import com.tangem.features.markets.ui.entity.SortByBottomSheetContentUM -import com.tangem.features.markets.ui.entity.SortByTypeUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByBottomSheetContentUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM @Composable fun MarketsListSortByBottomSheet(config: TangemBottomSheetConfig) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/UnableToLoadData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt similarity index 96% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/UnableToLoadData.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt index cf58422fb5..4cec85a065 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/UnableToLoadData.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.ui.components +package com.tangem.features.markets.tokenlist.impl.ui.components import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListItemUM.kt similarity index 94% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListItemUM.kt index a561cf8517..d96e5f8e21 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListItemUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.ui.entity +package com.tangem.features.markets.tokenlist.impl.ui.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartLook diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListUM.kt similarity index 94% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListUM.kt index 9167060698..39d29633d4 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.ui.entity +package com.tangem.features.markets.tokenlist.impl.ui.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig @@ -46,6 +46,7 @@ sealed class ListUM { val visibleIdsChanged: (List) -> Unit, val onShowTokensUnder100kClicked: () -> Unit, val triggerScrollReset: StateEvent, + val onItemClick: (MarketsListItemUM) -> Unit, ) : ListUM() data object Loading : ListUM() diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/SortByBottomSheetContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/SortByBottomSheetContentUM.kt similarity index 79% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/SortByBottomSheetContentUM.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/SortByBottomSheetContentUM.kt index af420055f5..fc3354f4b9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/SortByBottomSheetContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/SortByBottomSheetContentUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.ui.entity +package com.tangem.features.markets.tokenlist.impl.ui.entity import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt similarity index 96% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt index 782d4bfbc8..fd5efe0400 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt @@ -1,10 +1,10 @@ @file:Suppress("MagicNumber") -package com.tangem.features.markets.ui.preview +package com.tangem.features.markets.tokenlist.impl.ui.preview import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.features.markets.ui.entity.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( collection = listOf( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index fa0bdbd72d..d671a0c9c5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -10,7 +10,7 @@ import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.features.markets.MarketsFeatureToggles -import com.tangem.features.markets.component.MarketsListComponent +import com.tangem.features.markets.component.MarketsEntryComponent import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.AndroidEntryPoint @@ -32,7 +32,7 @@ internal class WalletFragment : ComposeFragment() { internal lateinit var walletRouter: WalletRouter @Inject - internal lateinit var marketsListComponentFactory: MarketsListComponent.Factory + internal lateinit var marketsEntryComponentFactory: MarketsEntryComponent.Factory @Inject internal lateinit var coroutineDispatcherProvider: CoroutineDispatcherProvider @@ -43,7 +43,7 @@ internal class WalletFragment : ComposeFragment() { @Inject internal lateinit var marketsFeatureToggles: MarketsFeatureToggles - private var marketsListComponent: MarketsListComponent? = null + private var marketsEntryComponent: MarketsEntryComponent? = null private val _walletRouter: InnerWalletRouter get() = requireNotNull(walletRouter as? InnerWalletRouter) { @@ -61,7 +61,7 @@ internal class WalletFragment : ComposeFragment() { hiltComponentBuilder = componentBuilder, ) - marketsListComponent = marketsListComponentFactory.create(appContext) + marketsEntryComponent = marketsEntryComponentFactory.create(appContext) } } @@ -69,7 +69,7 @@ internal class WalletFragment : ComposeFragment() { override fun ScreenContent(modifier: Modifier) { _walletRouter.Initialize( onFinish = requireActivity()::finish, - marketsListComponent = marketsListComponent, + marketsEntryComponent = marketsEntryComponent, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index c90cea3084..2ee8c80a31 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -25,7 +25,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScree import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel -import com.tangem.features.markets.component.MarketsListComponent +import com.tangem.features.markets.component.MarketsEntryComponent import kotlin.properties.Delegates /** Default implementation of wallet feature router */ @@ -41,7 +41,7 @@ internal class DefaultWalletRouter( override fun getEntryFragment(): Fragment = WalletFragment.create() @Composable - override fun Initialize(onFinish: () -> Unit, marketsListComponent: MarketsListComponent?) { + override fun Initialize(onFinish: () -> Unit, marketsEntryComponent: MarketsEntryComponent?) { this.onFinish = onFinish NavHost( @@ -56,7 +56,7 @@ internal class DefaultWalletRouter( WalletScreen( state = viewModel.uiState.collectAsStateWithLifecycle().value, - marketsListComponent = marketsListComponent, + marketsEntryComponent = marketsEntryComponent, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 0666e1191a..ee4229c970 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.markets.component.MarketsListComponent +import com.tangem.features.markets.component.MarketsEntryComponent import com.tangem.features.wallet.navigation.WalletRouter /** @@ -24,7 +24,7 @@ internal interface InnerWalletRouter : WalletRouter { * @param onFinish finish activity callback */ @Composable - fun Initialize(onFinish: () -> Unit, marketsListComponent: MarketsListComponent?) + fun Initialize(onFinish: () -> Unit, marketsEntryComponent: MarketsEntryComponent?) /** Pop back stack */ fun popBackStack() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 41698402f7..9e02bbe2c8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -70,12 +70,12 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balances import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator import com.tangem.features.markets.component.BottomSheetState -import com.tangem.features.markets.component.MarketsListComponent +import com.tangem.features.markets.component.MarketsEntryComponent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch @Composable -internal fun WalletScreen(state: WalletScreenState, marketsListComponent: MarketsListComponent?) { +internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent?) { BackHandler(onBack = state.onBackClick) // It means that screen is still initializing @@ -98,7 +98,7 @@ internal fun WalletScreen(state: WalletScreenState, marketsListComponent: Market snackbarHostState = snackbarHostState, isAutoScroll = isAutoScroll, onAutoScrollReset = { isAutoScroll.value = false }, - marketsListComponent = marketsListComponent, + marketsEntryComponent = marketsEntryComponent, alertConfig = alertConfig, ) @@ -119,7 +119,7 @@ private fun WalletContent( walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, - marketsListComponent: MarketsListComponent?, + marketsEntryComponent: MarketsEntryComponent?, alertConfig: WalletAlertState?, onAutoScrollReset: () -> Unit, ) { @@ -216,7 +216,7 @@ private fun WalletContent( ) } - if (marketsListComponent != null) { + if (marketsEntryComponent != null) { val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } @@ -232,7 +232,7 @@ private fun WalletContent( alertConfig = alertConfig, onBottomSheetStateChange = { bottomSheetState.value = it }, bottomSheetContent = { - marketsListComponent.BottomSheetContent( + marketsEntryComponent.BottomSheetContent( bottomSheetState = bottomSheetState, onHeaderSizeChange = { headerSize = it }, modifier = Modifier, @@ -638,7 +638,7 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider:: TangemThemePreview { WalletScreen( state = data, - marketsListComponent = null, + marketsEntryComponent = null, ) } } From 1451f23cb2ec1b89953847ba00616e8e82dbe6a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jul 2024 14:18:41 +0300 Subject: [PATCH 06/87] Updated on 2026-08-14 --- .../tap/common/redux/global/GlobalAction.kt | 3 - .../tap/common/redux/global/GlobalReducer.kt | 3 - .../tap/di/domain/SettingsDomainModule.kt | 6 + .../details/redux/DetailsMiddleware.kt | 2 - .../tangem/tap/features/home/HomeFragment.kt | 37 +++-- .../tangem/tap/features/home/HomeViewModel.kt | 131 ++++++++++++++++++ .../home/featuretoggles/HomeFeatureToggles.kt | 12 ++ .../tap/features/home/redux/HomeAction.kt | 13 +- .../tap/features/home/redux/HomeMiddleware.kt | 5 +- .../tap/features/home/redux/HomeReducer.kt | 7 - .../configs/feature_toggles_config.json | 4 + .../settings/ShouldSaveAccessCodesUseCase.kt | 8 ++ 12 files changed, 194 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt create mode 100644 app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/ShouldSaveAccessCodesUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index e7fba06fb7..c35ddefdd8 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -5,7 +5,6 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.feedback.FeedbackData @@ -95,6 +94,4 @@ sealed class GlobalAction : Action { object FetchUserCountry : GlobalAction() { data class Success(val countryCode: String) : GlobalAction() } - - data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index d85d35f457..043426c2de 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -92,9 +92,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde userCountryCode = action.countryCode, ) } - is GlobalAction.ChangeAppThemeMode -> globalState.copy( - appThemeMode = action.appThemeMode, - ) else -> globalState } } \ No newline at end of file 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 7ff162a0d1..af14718da8 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 @@ -221,5 +221,11 @@ internal object SettingsDomainModule { ): NeverRequestPermissionUseCase { return NeverRequestPermissionUseCase(repository = permissionRepository) } + + @Provides + @Singleton + fun provideShouldSaveAccessCodesUseCase(settingsRepository: SettingsRepository): ShouldSaveAccessCodesUseCase { + return ShouldSaveAccessCodesUseCase(settingsRepository = settingsRepository) + } // endregion } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 689263efaf..a72851ef35 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -134,8 +134,6 @@ class DetailsMiddleware { scope.launch { repository.changeAppThemeMode(appThemeMode) - - store.dispatchWithMain(GlobalAction.ChangeAppThemeMode(appThemeMode)) } } diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index f3fae68135..a39f6b2278 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier +import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics - import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsIconsDisposable import com.tangem.core.ui.screen.ComposeFragment @@ -18,6 +18,7 @@ import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.compose.StoriesScreen +import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.store @@ -26,17 +27,21 @@ import org.rekotlin.StoreSubscriber import javax.inject.Inject @AndroidEntryPoint -class HomeFragment : ComposeFragment(), StoreSubscriber { +internal class HomeFragment : ComposeFragment(), StoreSubscriber { @Inject override lateinit var uiDependencies: UiDependencies + @Inject + lateinit var homeFeatureToggles: HomeFeatureToggles + private var homeState: MutableState = mutableStateOf(store.state.homeState) + private val viewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) store.dispatch(HomeAction.OnCreate) - store.dispatch(HomeAction.Init) } @Composable @@ -73,17 +78,29 @@ class HomeFragment : ComposeFragment(), StoreSubscriber { StoriesScreen( homeState = homeState, onScanButtonClick = { - Analytics.send(IntroductionProcess.ButtonScanCard()) - store.dispatch(action = HomeAction.ReadCard(scope = requireActivity().lifecycleScope)) + if (homeFeatureToggles.isCallbacksRefactoringEnabled) { + viewModel.onScanClick() + } else { + Analytics.send(IntroductionProcess.ButtonScanCard()) + store.dispatch(action = HomeAction.ReadCard(scope = requireActivity().lifecycleScope)) + } }, onShopButtonClick = { - Analytics.send(IntroductionProcess.ButtonBuyCards()) - store.dispatch(HomeAction.GoToShop(store.state.globalState.userCountryCode)) + if (homeFeatureToggles.isCallbacksRefactoringEnabled) { + viewModel.onShopClick() + } else { + Analytics.send(IntroductionProcess.ButtonBuyCards()) + store.dispatch(HomeAction.GoToShop) + } }, onSearchTokensClick = { - Analytics.send(IntroductionProcess.ButtonTokensList()) - store.dispatchNavigationAction { push(AppRoute.ManageTokens) } - store.dispatch(TokensAction.SetArgs.ReadAccess) + if (homeFeatureToggles.isCallbacksRefactoringEnabled) { + viewModel.onSearchClick() + } else { + Analytics.send(IntroductionProcess.ButtonTokensList()) + store.dispatchNavigationAction { push(AppRoute.ManageTokens) } + store.dispatch(TokensAction.SetArgs.ReadAccess) + } }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt new file mode 100644 index 0000000000..f68dac30ca --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt @@ -0,0 +1,131 @@ +package com.tangem.tap.features.home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.ShouldSaveAccessCodesUseCase +import com.tangem.domain.tokens.TokensAction +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter +import com.tangem.tap.common.analytics.events.IntroductionProcess +import com.tangem.tap.common.analytics.events.Shop +import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.tap.common.extensions.onUserWalletSelected +import com.tangem.tap.features.home.redux.HIDE_PROGRESS_DELAY +import com.tangem.tap.features.home.redux.HomeAction +import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL +import com.tangem.tap.store +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@Suppress("LongParameterList") +@HiltViewModel +internal class HomeViewModel @Inject constructor( + private val shouldSaveAccessCodesUseCase: ShouldSaveAccessCodesUseCase, + private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, + private val scanCardProcessor: ScanCardProcessor, + private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val saveWalletUseCase: SaveWalletUseCase, + private val urlOpener: UrlOpener, + private val analyticsEventHandler: AnalyticsEventHandler, +) : ViewModel() { + + fun onScanClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard()) + scanCard() + } + + fun onShopClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards()) + analyticsEventHandler.send(Shop.ScreenOpened()) + + urlOpener.openUrl(NEW_BUY_WALLET_URL) + } + + fun onSearchClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonTokensList()) + + store.dispatch(TokensAction.SetArgs.ReadAccess) + store.dispatchNavigationAction { push(AppRoute.ManageTokens) } + } + + private fun scanCard() { + viewModelScope.launch { + setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = shouldSaveAccessCodesUseCase()) + + scanCardProcessor.scan( + analyticsSource = AnalyticsParam.ScreensSources.Intro, + onProgressStateChange = { showProgress -> + if (showProgress) { + store.dispatch(HomeAction.ScanInProgress(scanInProgress = true)) + } else { + delay(HIDE_PROGRESS_DELAY) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) + } + }, + onFailure = { + Timber.e(it, "Unable to scan card") + delay(HIDE_PROGRESS_DELAY) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) + }, + onSuccess = ::proceedWithScanResponse, + ) + } + } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { + val userWallet = UserWalletBuilder( + scanResponse = scanResponse, + generateWalletNameUseCase = generateWalletNameUseCase, + ).build() + + if (userWallet == null) { + Timber.e("User wallet not created") + return + } + + saveWalletUseCase(userWallet).fold( + ifLeft = { Timber.e(it.toString(), "Unable to save user wallet") }, + ifRight = { + sendSignedInCardAnalyticsEvent(scanResponse) + coroutineScope { store.onUserWalletSelected(userWallet = userWallet) } + }, + ) + + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) + delay(HIDE_PROGRESS_DELAY) + + store.dispatchNavigationAction { push(AppRoute.Wallet) } + } + + private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) + + if (currency != null) { + Analytics.send( + event = Basic.SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = Basic.SignedIn.SignInType.Card, + walletsCount = "1", + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt new file mode 100644 index 0000000000..8d23463a53 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.features.home.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import javax.inject.Inject + +internal class HomeFeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) { + + val isCallbacksRefactoringEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "HOME_SCREEN_CALLBACKS_REFACTORING_ENABLED") +} \ 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 2266971dad..f0171194c2 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 @@ -5,22 +5,17 @@ import org.rekotlin.Action sealed class HomeAction : Action { - object OnCreate : HomeAction() - object Init : HomeAction() - - data class InsertStory(val position: Int, val story: Stories) : HomeAction() + data object OnCreate : HomeAction() /** * Action for scanning card * - * @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed + * @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed */ - data class ReadCard( - val scope: CoroutineScope, - ) : HomeAction() + data class ReadCard(val scope: CoroutineScope) : HomeAction() data class ScanInProgress(val scanInProgress: Boolean) : HomeAction() - data class GoToShop(val userCountryCode: String?) : HomeAction() + data object GoToShop : HomeAction() data class UpdateCountryCode(val userCountryCode: String) : 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 203a2f983f..a2a3757628 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 @@ -27,7 +27,7 @@ import org.rekotlin.Action import org.rekotlin.Middleware import timber.log.Timber -private const val HIDE_PROGRESS_DELAY = 400L +internal const val HIDE_PROGRESS_DELAY = 400L object HomeMiddleware { val handler = homeMiddleware @@ -49,8 +49,7 @@ private fun handleHomeAction(action: Action) { is HomeAction.OnCreate -> { Analytics.eraseContext() Analytics.send(IntroductionProcess.ScreenOpened()) - } - is HomeAction.Init -> { + store.dispatch(GlobalAction.RestoreAppCurrency) store.dispatch(GlobalAction.ExchangeManager.Init) store.dispatch(GlobalAction.FetchUserCountry) 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 bc71e1fef2..f9d977fdec 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 @@ -12,13 +12,6 @@ private fun internalReduce(action: Action, appState: AppState): HomeState { var state = appState.homeState when (action) { - is HomeAction.InsertStory -> { - state = state.copy( - stories = state.stories.toMutableList().apply { - add(action.position, action.story) - }, - ) - } is HomeAction.ScanInProgress -> { state = state.copy(scanInProgress = action.scanInProgress) } 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 7def5136f7..7a5cc57245 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -38,5 +38,9 @@ { "name": "MARKETS_ENABLED", "version": "undefined" + }, + { + "name": "HOME_SCREEN_CALLBACKS_REFACTORING_ENABLED", + "version": "5.14.0" } ] diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldSaveAccessCodesUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldSaveAccessCodesUseCase.kt new file mode 100644 index 0000000000..b68ede4106 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldSaveAccessCodesUseCase.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +class ShouldSaveAccessCodesUseCase(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke(): Boolean = settingsRepository.shouldSaveAccessCodes() +} \ No newline at end of file From ff2907e26e39c6caeda1cf5675b5d6f1fb945d7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jul 2024 17:45:03 +0300 Subject: [PATCH 07/87] Updated on 2026-08-14 --- .../data/tokens/repository/DefaultNetworksRepository.kt | 7 ++++++- gradle/dependencies.toml | 2 +- .../java/com/tangem/blockchainsdk/utils/Blockchain.kt | 8 +++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index bcc65da983..58aa0a6dd6 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -100,7 +100,7 @@ internal class DefaultNetworksRepository( override fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean { val blockchain = Blockchain.fromNetworkId(network.id.value) - return blockchain == Blockchain.Aptos + return REQUIRED_ACCOUNT_WITHOUT_RESERVE_BLOCKCHAINS.contains(blockchain) } override suspend fun getNetworkAddresses( @@ -345,4 +345,9 @@ internal class DefaultNetworksRepository( private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, network: Network): String { return "network_status_${userWalletId}_${network.id.value}_${network.derivationPath.value}" } + + private companion object { + + val REQUIRED_ACCOUNT_WITHOUT_RESERVE_BLOCKCHAINS = listOf(Blockchain.Aptos, Blockchain.Filecoin) + } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index d6bb419a92..377070533f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-703" +tangemBlockchainSdk = "develop-710" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 1c22bba006..cd640e4796 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -118,6 +118,7 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "koinos/test" -> Blockchain.KoinosTestnet "joystream" -> Blockchain.Joystream "bittensor" -> Blockchain.Bittensor + "filecoin" -> Blockchain.Filecoin else -> null } } @@ -237,6 +238,7 @@ fun Blockchain.toNetworkId(): String { Blockchain.KoinosTestnet -> "koinos/test" Blockchain.Joystream -> "joystream" Blockchain.Bittensor -> "bittensor" + Blockchain.Filecoin -> "filecoin" } } @@ -313,6 +315,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Koinos, Blockchain.KoinosTestnet -> "koinos" Blockchain.Joystream -> "joystream" Blockchain.Bittensor -> "bittensor" + Blockchain.Filecoin -> "filecoin" } } @@ -325,7 +328,9 @@ fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? { Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(NODL_AMOUNT_TO_CREATE_ACCOUNT) else BigDecimal.ONE Blockchain.XRP -> BigDecimal.TEN Blockchain.Near, Blockchain.NearTestnet -> 0.00182.toBigDecimal() - Blockchain.Aptos, Blockchain.AptosTestnet -> BigDecimal.ZERO + Blockchain.Aptos, Blockchain.AptosTestnet, + Blockchain.Filecoin, + -> BigDecimal.ZERO else -> null } } @@ -343,4 +348,5 @@ private val excludedBlockchains = listOf( Blockchain.NexaTestnet, Blockchain.Mantle, Blockchain.MantleTestnet, + Blockchain.Filecoin, ) \ No newline at end of file From 957bb0dd56e54f4205b3890ccbf2fe785490aeae Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Jul 2024 18:44:09 +0500 Subject: [PATCH 08/87] Updated on 2026-08-14 --- data/staking/build.gradle.kts | 3 + .../data/staking/DefaultStakingRepository.kt | 86 ++++++++++++++++--- .../data/staking/di/StakingDataModule.kt | 3 + .../stakekit/transaction/ActionParams.kt | 1 + .../domain/staking/EstimateGasUseCase.kt | 10 ++- .../staking/GetStakingTransactionUseCase.kt | 10 ++- .../staking/repositories/StakingRepository.kt | 4 +- .../repository/MockStakingRepository.kt | 12 ++- .../converters/YieldBalancesConverter.kt | 2 +- .../amount/AmountChangeStateTransformer.kt | 12 ++- .../amount/AmountMaxValueStateTransformer.kt | 15 ++-- .../AmountRequirementStateTransformer.kt | 58 +++++++------ .../viewmodel/StakingViewModel.kt | 4 + 13 files changed, 159 insertions(+), 61 deletions(-) diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 0a1688dee1..017cc43973 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.staking) implementation(projects.domain.wallets.models) + implementation(projects.domain.legacy) /** Feature Api modules */ implementation(projects.features.staking.api) @@ -40,6 +41,8 @@ dependencies { implementation(projects.libs.blockchainSdk) + + implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index bc015d17b5..1a2bc1999c 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -1,8 +1,10 @@ package com.tangem.data.staking +import android.util.Base64 import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.common.extensions.toCompressedPublicKey import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.staking.converters.* import com.tangem.data.staking.converters.action.ActionStatusConverter @@ -23,7 +25,10 @@ import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lceFlow -import com.tangem.domain.staking.model.* +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata +import com.tangem.domain.staking.model.stakekit.NetworkType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList @@ -37,10 +42,10 @@ import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.toFormattedString import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -55,6 +60,7 @@ internal class DefaultStakingRepository( private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, private val stakingFeatureToggle: StakingFeatureToggles, + private val walletManagersFacade: WalletManagersFacade, ) : StakingRepository { private val stakingNetworkTypeConverter = StakingNetworkTypeConverter() @@ -160,11 +166,27 @@ internal class DefaultStakingRepository( } } - override suspend fun createAction(params: ActionParams): StakingAction { + override suspend fun createAction( + userWalletId: UserWalletId, + network: Network, + params: ActionParams, + ): StakingAction { return withContext(dispatchers.io) { val response = when (params.actionCommonType) { - StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(createActionRequestBody(params)) - StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(createActionRequestBody(params)) + StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction( + createActionRequestBody( + userWalletId, + network, + params, + ), + ) + StakingActionCommonType.EXIT -> stakeKitApi.createExitAction( + createActionRequestBody( + userWalletId, + network, + params, + ), + ) StakingActionCommonType.PENDING -> stakeKitApi.createPendingAction( createPendingActionRequestBody(params), ) @@ -174,11 +196,27 @@ internal class DefaultStakingRepository( } } - override suspend fun estimateGas(params: ActionParams): StakingGasEstimate { + override suspend fun estimateGas( + userWalletId: UserWalletId, + network: Network, + params: ActionParams, + ): StakingGasEstimate { return withContext(dispatchers.io) { val gasEstimateDTO = when (params.actionCommonType) { - StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(createActionRequestBody(params)) - StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(createActionRequestBody(params)) + StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter( + createActionRequestBody( + userWalletId, + network, + params, + ), + ) + StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit( + createActionRequestBody( + userWalletId, + network, + params, + ), + ) StakingActionCommonType.PENDING -> stakeKitApi.estimateGasOnPending( createPendingActionRequestBody(params), ) @@ -435,12 +473,19 @@ internal class DefaultStakingRepository( } } - private fun createActionRequestBody(params: ActionParams): ActionRequestBody { + private suspend fun createActionRequestBody( + userWalletId: UserWalletId, + network: Network, + params: ActionParams, + ): ActionRequestBody { return ActionRequestBody( integrationId = params.integrationId, - addresses = Address(params.address), + addresses = Address( + address = params.address, + additionalAddresses = createAdditionalAddresses(userWalletId, network, params), + ), args = ActionRequestBodyArgs( - amount = params.amount.toFormattedString(params.token.decimals), + amount = params.amount.toPlainString(), inputToken = tokenConverter.convertBack(params.token), validatorAddress = params.validatorAddress, ), @@ -453,12 +498,29 @@ internal class DefaultStakingRepository( type = params.type ?: StakingActionType.UNKNOWN, passthrough = params.passthrough.orEmpty(), args = ActionRequestBodyArgs( - amount = params.amount.toFormattedString(params.token.decimals), + amount = params.amount.toPlainString(), validatorAddress = params.validatorAddress, ), ) } + private suspend fun createAdditionalAddresses( + userWalletId: UserWalletId, + network: Network, + params: ActionParams, + ): Address.AdditionalAddresses? { + val selectedWallet = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) + return when (params.token.network) { + NetworkType.COSMOS -> Address.AdditionalAddresses( + cosmosPubKey = Base64.encodeToString( + /* input = */ selectedWallet?.wallet?.publicKey?.blockchainKey?.toCompressedPublicKey(), + /* flags = */ Base64.NO_WRAP, + ), + ) + else -> null + } + } + override fun isStakeMoreAvailable(networkId: Network.ID): Boolean { val blockchain = Blockchain.fromId(networkId.value) return when (blockchain) { diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index f0562851c1..d261cdece4 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -13,6 +13,7 @@ import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -35,6 +36,7 @@ internal object StakingDataModule { dispatchers: CoroutineDispatcherProvider, stakingFeatureToggle: StakingFeatureToggles, cacheRegistry: CacheRegistry, + walletManagersFacade: WalletManagersFacade, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, @@ -44,6 +46,7 @@ internal object StakingDataModule { dispatchers = dispatchers, cacheRegistry = cacheRegistry, stakingFeatureToggle = stakingFeatureToggle, + walletManagersFacade = walletManagersFacade, ) } diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt index 2adfaed13b..605ed69fa5 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt @@ -12,6 +12,7 @@ data class ActionParams( val address: String, val validatorAddress: String, val token: Token, + val publicKey: String? = null, val passthrough: String? = null, val type: StakingActionType? = null, ) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/EstimateGasUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/EstimateGasUseCase.kt index 381308b7a7..e5d79ff1b8 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/EstimateGasUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/EstimateGasUseCase.kt @@ -6,6 +6,8 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId /** * Use case for staking gas estimation. @@ -15,9 +17,13 @@ class EstimateGasUseCase( private val stakingErrorResolver: StakingErrorResolver, ) { - suspend operator fun invoke(params: ActionParams): Either { + suspend operator fun invoke( + userWalletId: UserWalletId, + network: Network, + params: ActionParams, + ): Either { return Either.catch { - stakingRepository.estimateGas(params) + stakingRepository.estimateGas(userWalletId, network, params) }.mapLeft { stakingErrorResolver.resolve(it) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt index d6d147b1c3..cfe2c9a254 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt @@ -6,6 +6,8 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.delay /** @@ -16,9 +18,13 @@ class GetStakingTransactionUseCase( private val stakingErrorResolver: StakingErrorResolver, ) { - suspend operator fun invoke(params: ActionParams): Either { + suspend operator fun invoke( + userWalletId: UserWalletId, + network: Network, + params: ActionParams, + ): Either { return Either.catch { - val createAction = stakingRepository.createAction(params) + val createAction = stakingRepository.createAction(userWalletId, network, params) // workaround, sometimes transaction is not created immediately after actions/enter delay(PATCH_TRANSACTION_REQUEST_DELAY) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index a9ff51285e..5605958156 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -61,9 +61,9 @@ interface StakingRepository { addresses: List, ): YieldBalanceList - suspend fun createAction(params: ActionParams): StakingAction + suspend fun createAction(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingAction - suspend fun estimateGas(params: ActionParams): StakingGasEstimate + suspend fun estimateGas(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingGasEstimate suspend fun constructTransaction(transactionId: String): StakingTransaction diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index ad8a88a0fd..75e83715bb 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -167,7 +167,11 @@ class MockStakingRepository : StakingRepository { balances = listOf(YieldBalance.Error), ) - override suspend fun createAction(params: ActionParams): StakingAction { + override suspend fun createAction( + userWalletId: UserWalletId, + network: Network, + params: ActionParams, + ): StakingAction { return StakingAction( id = "quis", integrationId = "persequeris", @@ -182,7 +186,11 @@ class MockStakingRepository : StakingRepository { ) } - override suspend fun estimateGas(params: ActionParams): StakingGasEstimate { + override suspend fun estimateGas( + userWalletId: UserWalletId, + network: Network, + params: ActionParams, + ): StakingGasEstimate { return StakingGasEstimate( amount = BigDecimal(0.0001), token = Token( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 23141275d2..739015eb13 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -113,7 +113,6 @@ internal class YieldBalancesConverter( private fun BalanceType.toGroup() = when (this) { BalanceType.PREPARING, BalanceType.STAKED, - BalanceType.REWARDS, BalanceType.AVAILABLE, BalanceType.LOCKED, -> BalanceGroupType.ACTIVE @@ -121,6 +120,7 @@ internal class YieldBalancesConverter( BalanceType.UNLOCKING, BalanceType.UNSTAKED, -> BalanceGroupType.UNSTAKED + BalanceType.REWARDS, BalanceType.UNKNOWN, -> BalanceGroupType.UNKNOWN } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt index 4efdb9141d..b87f8a0251 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -12,13 +12,11 @@ internal class AmountChangeStateTransformer( private val value: String, ) : Transformer { - private val amountRequirementStateTransformer by lazy(LazyThreadSafetyMode.NONE) { - AmountRequirementStateTransformer( - cryptoCurrencyStatus, - yield, - value, - ) - } + private val amountRequirementStateTransformer = AmountRequirementStateTransformer( + cryptoCurrencyStatus, + yield, + value, + ) override fun transform(prevState: StakingUiState): StakingUiState { val updatedAmountState = AmountFieldChangeTransformer( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt index 8270ef3971..ae53eab63e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -12,16 +12,13 @@ internal class AmountMaxValueStateTransformer( private val yield: Yield, ) : Transformer { - private val amountRequirementStateTransformer by lazy(LazyThreadSafetyMode.NONE) { - val value = cryptoCurrencyStatus.value.amount + private val amountRequirementStateTransformer = AmountRequirementStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + yield = yield, + value = cryptoCurrencyStatus.value.amount ?.parseBigDecimal(cryptoCurrencyStatus.currency.decimals) - .orEmpty() - AmountRequirementStateTransformer( - cryptoCurrencyStatus, - yield, - value, - ) - } + .orEmpty(), + ) override fun transform(prevState: StakingUiState): StakingUiState { val updatedAmountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt index 72d7a6cb2d..f16597ae54 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.staking.model.stakekit.AddressArgument import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R @@ -18,38 +19,47 @@ internal class AmountRequirementStateTransformer( ) : Transformer { override fun transform(prevState: AmountState): AmountState { val amountRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT] - val amountDecimal = value.parseToBigDecimal(cryptoCurrencyStatus.currency.decimals) return if (prevState !is AmountState.Data || amountRequirements == null) { prevState } else { - val isAlreadyErrorState = prevState.amountTextField.isError - val isAmountRequired = amountRequirements.required - val isAmountZero = amountDecimal.isZero() - val isExceedsRequirements = - amountRequirements.maximum?.compareTo(amountDecimal) == -1 || - amountRequirements.minimum?.compareTo(amountDecimal) == 1 + updateWithError(prevState, amountRequirements) + } + } - val isRequirementError = !isAmountZero && isAmountRequired && isExceedsRequirements && !isAlreadyErrorState - if (isRequirementError) { - prevState.copy( - amountTextField = prevState.amountTextField.copy( - isError = true, - error = resourceReference( - R.string.staking_amount_requirement_error, - wrappedList( - BigDecimalFormatter.formatCryptoAmount( - amountRequirements.minimum, - cryptoCurrencyStatus.currency.symbol, - cryptoCurrencyStatus.currency.decimals, - ), + private fun updateWithError(prevState: AmountState.Data, amountRequirements: AddressArgument): AmountState { + val isRequirementError = isRequirementError(prevState, amountRequirements) + return if (isRequirementError) { + prevState.copy( + amountTextField = prevState.amountTextField.copy( + isError = true, + error = resourceReference( + R.string.staking_amount_requirement_error, + wrappedList( + BigDecimalFormatter.formatCryptoAmount( + amountRequirements.minimum, + cryptoCurrencyStatus.currency.symbol, + cryptoCurrencyStatus.currency.decimals, ), ), ), - ) - } else { - prevState - } + ), + ) + } else { + prevState } } + + private fun isRequirementError(prevState: AmountState.Data, amountRequirements: AddressArgument): Boolean { + val amountDecimal = value.parseToBigDecimal(cryptoCurrencyStatus.currency.decimals) + + val isAlreadyErrorState = prevState.amountTextField.isError + val isAmountRequired = amountRequirements.required + val isAmountZero = amountDecimal.isZero() + val isExceedsRequirements = + amountRequirements.maximum?.compareTo(amountDecimal) == -1 || + amountRequirements.minimum?.compareTo(amountDecimal) == 1 + + return !isAmountZero && isAmountRequired && isExceedsRequirements && !isAlreadyErrorState + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 5459256c1d..c656944417 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -120,6 +120,8 @@ internal class StakingViewModel @Inject constructor( val pendingActions = confirmationState.pendingActions val stakingTransaction = getStakingTransactionUseCase( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, params = ActionParams( actionCommonType = getStakingCommonType(), integrationId = yield.id, @@ -158,6 +160,8 @@ internal class StakingViewModel @Inject constructor( val cryptoCurrencyValue = cryptoCurrencyStatus.value val stakingGasEstimate = estimateGasUseCase( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, params = ActionParams( actionCommonType = getStakingCommonType(), integrationId = yield.id, From f25889d1a01a0c73ca48e686af5e74cd61346e31 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jul 2024 11:03:53 +0200 Subject: [PATCH 09/87] Updated on 2026-08-14 --- app/build.gradle.kts | 12 ---- .../com/tangem/tap/LockUserWalletsTimer.kt | 5 +- .../java/com/tangem/tap/TangemApplication.kt | 10 +-- .../com/tangem/tap/common/chat/ChatManager.kt | 23 ------- .../tap/common/chat/opener/ChatOpener.kt | 8 --- .../implementation/SprinklrChatOpener.kt | 57 ----------------- .../common/feedback/LegacyFeedbackManager.kt | 42 ------------- .../tap/common/redux/global/GlobalAction.kt | 2 - .../common/redux/global/GlobalMiddleware.kt | 20 ------ .../tap/data/RuntimeUserWalletsStore.kt | 8 ++- .../card/DefaultDerivationsRepository.kt | 17 +++-- .../details/redux/DetailsMiddleware.kt | 2 +- .../ui/cardsettings/CardSettingsViewModel.kt | 18 ++++-- .../details/ui/common/utils/ResetToFactory.kt | 5 +- .../ui/resetcard/ResetCardViewModel.kt | 33 ++++------ .../ui/dialogs/AttestationFailedDialog.kt | 2 +- .../viewmodels/TokensListMigration.kt | 1 + .../viewmodels/TokensListViewModel.kt | 4 -- .../card/DefaultDerivationsRepositoryTest.kt | 6 +- .../com/tangem/common/routing/AppRoute.kt | 29 ++++++--- .../local/userwallet/UserWalletsStore.kt | 7 ++- .../com/tangem/domain/models/scan/CardDTO.kt | 9 --- .../component/impl/DefaultDetailsComponent.kt | 2 +- .../features/details/utils/UserWalletSaver.kt | 63 ++++++++++++------- .../disclaimer/impl/ui/DisclaimerScreen.kt | 10 ++- .../tokendetails/ui/TokenDetailsScreen.kt | 13 +++- .../presentation/wallet/ui/WalletScreen.kt | 4 +- .../wallet/viewmodels/WalletViewModel.kt | 2 + gradle/dependencies.toml | 1 - .../extension/AppExtensionConfigurations.kt | 9 ++- 30 files changed, 149 insertions(+), 275 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bbe71b61be..a003607888 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,20 +27,12 @@ configurations.all { resolutionStrategy { dependencySubstitution { - substitute(module("com.facebook.react:react-native")) - .using(module("com.facebook.react:react-android:0.72.4")) - - substitute(module("com.facebook.react:hermes-engine")) - .using(module("com.facebook.react:hermes-android:0.72.4")) - substitute(module("org.bouncycastle:bcprov-jdk15on")) .using(module("org.bouncycastle:bcprov-jdk18on:1.73")) } force( "org.bouncycastle:bcpkix-jdk15on:1.70", - "com.facebook.react:react-android:0.72.4", - "com.facebook.react:hermes-android:0.72.4", ) } } @@ -224,10 +216,6 @@ dependencies { implementation(deps.walletConnectCore) implementation(deps.walletConnectWeb3) implementation(deps.prettyLogger) - implementation("com.facebook.react:react-android:0.72.4") - implementation(deps.sprClient) { - exclude(group = "com.github.stephenc.jcip") - } /** Testing libraries */ testImplementation(deps.test.coroutine) diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 8a80aff9f7..8758b045e3 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.utils.popTo import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable @@ -49,7 +48,7 @@ internal class LockUserWalletsTimer( start() if (shouldOpenWelcomeScreenOnResume) { - store.dispatchNavigationAction { popTo() } + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false) } } @@ -127,7 +126,7 @@ internal class LockUserWalletsTimer( if (wasApplicationStopped) { settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) } else { - store.dispatchNavigationAction { popTo() } + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } } } } diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 9a6577a66f..eaca995355 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -52,7 +52,6 @@ import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler -import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.feedback.AdditionalFeedbackInfo import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.common.images.createCoilImageLoader @@ -316,7 +315,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private fun initWithConfigDependency(config: Config) { initAnalytics(this, config) - initFeedbackManager(this, foregroundActivityObserver, store) + initFeedbackManager(this, store) } private fun initAnalytics(application: Application, config: Config) { @@ -337,11 +336,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { // ExceptionHandler.append(blockchainExceptionHandler) TODO: [REDACTED_JIRA] } - private fun initFeedbackManager( - context: Context, - foregroundActivityObserver: ForegroundActivityObserver, - store: Store, - ) { + private fun initFeedbackManager(context: Context, store: Store) { fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo { return AdditionalFeedbackInfo().apply { appVersion = try { @@ -382,7 +377,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { val feedbackManager = LegacyFeedbackManager( infoHolder = additionalFeedbackInfo, logCollector = tangemLogCollector, - chatManager = ChatManager(foregroundActivityObserver), feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, getFeedbackEmailUseCase = getFeedbackEmailUseCase, ) diff --git a/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt b/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt deleted file mode 100644 index 5bbe3fc679..0000000000 --- a/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.common.chat - -import android.content.Context -import com.tangem.datasource.config.models.ChatConfig -import com.tangem.datasource.config.models.SprinklrConfig -import com.tangem.tap.ForegroundActivityObserver -import com.tangem.tap.common.chat.opener.ChatOpener -import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener -import java.io.File - -class ChatManager(private val foregroundActivityObserver: ForegroundActivityObserver) { - private val openers = mutableMapOf() - - fun open(config: ChatConfig, createLogsFile: (Context) -> File?, createFeedbackFile: (Context) -> File?) { - val opener = openers.getOrPut(config) { - when (config) { - is SprinklrConfig -> SprinklrChatOpener(config, foregroundActivityObserver) - } - } - - opener.open(createFeedbackFile, createLogsFile) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt deleted file mode 100644 index aff21cad05..0000000000 --- a/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.tap.common.chat.opener - -import android.content.Context -import java.io.File - -internal interface ChatOpener { - fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt deleted file mode 100644 index 0fcb2a4cf2..0000000000 --- a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.tap.common.chat.opener.implementation - -import android.annotation.SuppressLint -import android.app.Application -import android.content.Context -import android.provider.Settings -import com.spr.messengerclient.config.SPRMessenger -import com.spr.messengerclient.config.bean.SPRMessengerConfig -import com.tangem.common.extensions.guard -import com.tangem.datasource.config.models.SprinklrConfig -import com.tangem.tap.ForegroundActivityObserver -import com.tangem.tap.common.chat.opener.ChatOpener -import timber.log.Timber -import java.io.File -import java.util.Locale - -internal class SprinklrChatOpener( - private val config: SprinklrConfig, - private val foregroundActivityObserver: ForegroundActivityObserver, -) : ChatOpener { - - override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) { - val messenger = SPRMessenger.shared() - - if (messenger.config == null) { - initSprConfig(messenger) - } - - messenger.startApplication() - } - - private fun initSprConfig(messenger: SPRMessenger) { - val application = foregroundActivityObserver.foregroundActivity?.application.guard { - Timber.e("The SPR chat cannot be opened because there are no activities in foreground") - return - } - - messenger.takeOff(application, createSprConfig(application, config)) - } - - @SuppressLint("HardwareIds") - private fun createSprConfig(application: Application, config: SprinklrConfig): SPRMessengerConfig { - return SPRMessengerConfig().apply { - appId = config.appId - appKey = CHAT_APP_KEY - deviceId = Settings.Secure.getString(application.contentResolver, Settings.Secure.ANDROID_ID) - environment = config.environment - skin = CHAT_SKIN - locale = Locale.getDefault().language - } - } - - private companion object { - const val CHAT_APP_KEY = "com.sprinklr.messenger.release" - const val CHAT_SKIN = "MODERN" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt index 9f2f6bb335..e7713995ac 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt @@ -2,13 +2,11 @@ package com.tangem.tap.common.feedback import android.content.Context import com.tangem.core.navigation.email.EmailSender -import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.common.TapWorkarounds import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.log.TangemLogCollector @@ -29,12 +27,10 @@ import java.io.StringWriter class LegacyFeedbackManager( val infoHolder: AdditionalFeedbackInfo, private val logCollector: TangemLogCollector, - private val chatManager: ChatManager, private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase, ) { - private var sessionFeedbackFile: File? = null private var sessionLogsFile: File? = null fun sendEmail(feedbackData: FeedbackData, scanResponse: ScanResponse?) { @@ -97,43 +93,6 @@ class LegacyFeedbackManager( } } - fun openChat(config: ChatConfig, feedbackData: FeedbackData) { - chatManager.open( - config = config, - createLogsFile = ::getLogFile, - createFeedbackFile = { context -> getFeedbackFile(context, feedbackData) }, - ) - } - - private fun getFeedbackFile(context: Context, feedbackData: FeedbackData): File? { - return try { - if (sessionFeedbackFile != null) { - return sessionFeedbackFile - } - val file = File(context.filesDir, FEEDBACK_FILE) - file.delete() - file.createNewFile() - - val feedback = feedbackData.run { - prepare(infoHolder) - joinTogether(context, infoHolder) - } - val fileWriter = FileWriter(file) - fileWriter.write(feedback) - fileWriter.close() - - if (file.exists()) { - sessionFeedbackFile = file - sessionFeedbackFile - } else { - null - } - } catch (ex: Exception) { - Timber.e(ex, "Can't create the logs file") - null - } - } - private fun getLogFile(context: Context): File? { return try { if (sessionLogsFile != null) { @@ -172,7 +131,6 @@ class LegacyFeedbackManager( private companion object { const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com" const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com" - const val FEEDBACK_FILE = "feedback.txt" const val LOGS_FILE = "logs.txt" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index c35ddefdd8..58d13a6d79 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -3,7 +3,6 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.datasource.config.ConfigManager -import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog @@ -79,7 +78,6 @@ sealed class GlobalAction : Action { data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction() data class SendEmail(val feedbackData: FeedbackData, val scanResponse: ScanResponse?) : GlobalAction() - data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction() object ExchangeManager : GlobalAction() { object Init : GlobalAction() { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index 957b756803..9c6882deda 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -74,26 +74,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) { scanResponse = action.scanResponse, ) } - is GlobalAction.OpenChat -> { - val globalState = store.state.globalState - val feedbackManager = globalState.feedbackManager.guard { - store.dispatchDebugErrorNotification("FeedbackManager not initialized") - return - } - val config = globalState.configManager?.config.guard { - store.dispatchDebugErrorNotification("Config not initialized") - return - } - - // if config not set -> try to get it based on a scanResponse.productType - val unsafeChatConfig = action.chatConfig ?: config.sprinklr - - val chatConfig = unsafeChatConfig.guard { - store.dispatchDebugErrorNotification("The chat config is not initialized") - return - } - feedbackManager.openChat(chatConfig, action.feedbackData) - } is GlobalAction.ExchangeManager.Init -> { val appStateSafe = appState() ?: return val config = appStateSafe.globalState.configManager?.config ?: return 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 58d13616e4..1661cedb1d 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -1,5 +1,6 @@ package com.tangem.tap.data +import com.tangem.common.CompletionResult import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet @@ -30,7 +31,10 @@ internal class RuntimeUserWalletsStore( return userWalletsListManager.userWallets.firstOrNull() } - override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) { - userWalletsListManager.update(userWalletId, update) + override suspend fun update( + userWalletId: UserWalletId, + update: suspend (UserWallet) -> UserWallet, + ): CompletionResult { + return userWalletsListManager.update(userWalletId, update) } } \ No newline at end of file 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 d7bb96e4c3..64e1b31472 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 @@ -19,7 +19,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching +import kotlinx.coroutines.withContext import timber.log.Timber internal typealias Derivations = Map> @@ -48,13 +48,12 @@ internal class DefaultDerivationsRepository( tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations) .doOnSuccess { response -> - updatePublicKeys(userWalletId = userWalletId, keys = response.entries).fold( - onSuccess = { - validateDerivations(userWallet.scanResponse, derivations) + updatePublicKeys(userWalletId = userWalletId, keys = response.entries) + .doOnSuccess { + validateDerivations(scanResponse = it.scanResponse, derivations = derivations) return - }, - onFailure = { throw it }, - ) + } + .doOnFailure { throw it } } .doOnFailure { throw it } @@ -99,8 +98,8 @@ internal class DefaultDerivationsRepository( } } - private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): Result { - return runCatching(dispatchers.io) { + private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): CompletionResult { + return withContext(dispatchers.io) { userWalletsStore.update( userWalletId = userWalletId, update = { userWallet -> userWallet.updateDerivedKeys(keys) }, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index a72851ef35..40f3d03a11 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -245,7 +245,7 @@ class DetailsMiddleware { deleteSavedAccessCodes() store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false) - store.dispatchNavigationAction { popTo() } + store.dispatchNavigationAction { replaceAll(AppRoute.Home) } return CompletionResult.Success(Unit) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 97d4a464d9..d8b9f44ca9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -109,7 +109,10 @@ internal class CardSettingsViewModel @Inject constructor( if (isResetCardAllowed) { CardInfo.ResetToFactorySettings( - description = getResetToFactoryDescription(card.backupStatus, cardTypesResolver), + description = getResetToFactoryDescription( + isActiveBackupStatus = card.backupStatus?.isActive == true, + typesResolver = cardTypesResolver, + ), ).let(::add) } } @@ -135,10 +138,15 @@ internal class CardSettingsViewModel @Inject constructor( push( route = AppRoute.ResetToFactory( userWalletId = userWalletId, - cardSpecificInfo = AppRoute.ResetToFactory.CardSpecificInfo( - cardId = card.cardId, - backupStatus = card.backupStatus, - ), + cardId = card.cardId, + isActiveBackupStatus = card.backupStatus?.isActive == true, + backupCardsCount = when (val status = card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + is CardDTO.BackupStatus.CardLinked, + CardDTO.BackupStatus.NoBackup, + null, + -> 0 + }, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt index 5be7b9eb88..b269ee6352 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt @@ -1,15 +1,14 @@ package com.tangem.tap.features.details.ui.common.utils import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.models.scan.CardDTO import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.wallet.R internal fun getResetToFactoryDescription( - backupStatus: CardDTO.BackupStatus?, + isActiveBackupStatus: Boolean, typesResolver: CardTypesResolver, ): TextReference { - return if (backupStatus?.isActive != true || typesResolver.isTangemTwins()) { + return if (!isActiveBackupStatus || typesResolver.isTangemTwins()) { TextReference.Res(R.string.reset_card_without_backup_to_factory_message) } else { TextReference.Res(R.string.reset_card_with_backup_to_factory_message) 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 2131cd42e2..41d0535147 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 @@ -13,7 +13,6 @@ import com.tangem.domain.card.DeleteSavedAccessCodesUseCase import com.tangem.domain.card.ResetCardUseCase import com.tangem.domain.card.ResetCardUserCodeParams import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UserWalletId @@ -65,8 +64,15 @@ internal class ResetCardViewModel @Inject constructor( // endregion // region Data of card that was scanned on CardSettings - private val primaryCardId: String - private val primaryBackupStatus: CardDTO.BackupStatus? + private val primaryCardId: String = savedStateHandle.get(AppRoute.ResetToFactory.CARD_ID) + ?: error("CardId must be provided for ResetCardViewModel") + + private val isActiveBackupPrimaryCard = + savedStateHandle.get(AppRoute.ResetToFactory.IS_ACTIVE_BACKUP_STATUS) + ?: error("IsActiveBackupCard must be provided for ResetCardViewModel") + + private val primaryBackupCardsCount = savedStateHandle.get(AppRoute.ResetToFactory.BACKUP_CARDS_COUNT) + ?: error("CardCount must be provided for ResetCardViewModel") // endregion // TODO: move logic to separate domain entity @@ -76,15 +82,6 @@ internal class ResetCardViewModel @Inject constructor( value = getInitialState(), ) - init { - val cardSpecificInfo = savedStateHandle.get(AppRoute.ResetToFactory.CARD_SPECIFIC_DATA) - ?.unbundle(AppRoute.ResetToFactory.CardSpecificInfo.serializer()) - ?: error("CardSpecificData must be provided for ResetCardViewModel") - - primaryCardId = cardSpecificInfo.cardId - primaryBackupStatus = cardSpecificInfo.backupStatus - } - private fun getInitialState(): ResetCardScreenState { val shouldShowResetPasswordButton = shouldShowResetPasswordButton() val warningsToShow = buildList { @@ -98,7 +95,7 @@ internal class ResetCardViewModel @Inject constructor( return ResetCardScreenState( resetButtonEnabled = false, descriptionText = getResetToFactoryDescription( - backupStatus = primaryBackupStatus, + isActiveBackupStatus = isActiveBackupPrimaryCard, typesResolver = currentCardTypesResolver, ), warningsToShow = warningsToShow, @@ -115,7 +112,7 @@ internal class ResetCardViewModel @Inject constructor( private fun shouldShowResetPasswordButton(): Boolean { val isTangemWallet = currentCardTypesResolver.isTangemWallet() || currentCardTypesResolver.isWallet2() - return isTangemWallet && primaryBackupStatus is CardDTO.BackupStatus.Active + return isTangemWallet && isActiveBackupPrimaryCard } private fun toggleFirstCondition(isAccepted: Boolean) { @@ -283,12 +280,6 @@ internal class ResetCardViewModel @Inject constructor( private fun getBackupCardsCount(): Int { if (!currentCardTypesResolver.isMultiwalletAllowed()) return 0 - return when (val status = primaryBackupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount - is CardDTO.BackupStatus.CardLinked, - is CardDTO.BackupStatus.NoBackup, - null, - -> 0 - } + return primaryBackupCardsCount } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt index 3e9df5885e..5b6c8b8c85 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt @@ -13,7 +13,7 @@ internal object AttestationFailedDialog { return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(R.string.common_error) setMessage(R.string.issuer_signature_loading_failed) - setPositiveButton(R.string.ok) { dialog, _ -> + setPositiveButton(R.string.common_ok) { dialog, _ -> dialog.dismiss() } setOnDismissListener { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt index b10741f96c..41495924b3 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt @@ -135,6 +135,7 @@ internal class TokensListMigration( derivePublicKeysUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList) .onRight { addCryptoCurrenciesUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList) + store.dispatchNavigationAction { popTo() } } .onLeft { Timber.e(it, "Failed to derive public keys") } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index a716c916bb..6e471d192d 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -11,8 +11,6 @@ import androidx.lifecycle.viewModelScope import androidx.paging.* import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.extensions.getGreyedOutIconRes @@ -26,7 +24,6 @@ import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.TokenWithBlockchain import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.fullNameWithoutTestnet import com.tangem.tap.common.extensions.getNetworkName import com.tangem.tap.features.customtoken.impl.presentation.models.SupportBlockchainType @@ -326,7 +323,6 @@ internal class TokensListViewModel @Inject constructor( ) uiState = state.copy(isSavingInProgress = false) - store.dispatchNavigationAction { popTo() } } } 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 fdf0ed8db2..12be6b889d 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 @@ -12,7 +12,9 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.* +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test @@ -144,7 +146,7 @@ internal class DefaultDerivationsRepositoryTest { coEvery { tangemSdkManager.derivePublicKeys(null, any()) } returns CompletionResult.Success( DerivationTaskResponse(DerivedKeysMocks.ethereumDerivedKeys), ) - coEvery { userWalletsStore.update(defaultUserWalletId, any()) } just Runs + coEvery { userWalletsStore.update(defaultUserWalletId, any()) } returns CompletionResult.Success(userWallet) runCatching { repository.derivePublicKeys( diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 8f9796de2b..8bb8395a7d 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -5,7 +5,6 @@ import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.navigation.Route -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency @@ -145,25 +144,35 @@ sealed class AppRoute(val path: String) : Route { data object AppSettings : AppRoute(path = "/app_settings") /** - * Reset to factory route + * Reset to factory * - * @property userWalletId user wallet id - * @property cardSpecificInfo info about card that was scanned on CardSettings + * @property userWalletId user wallet id + * @property cardId reset card id + * @property isActiveBackupStatus reset backup card status + * @property backupCardsCount backup cards count */ @Serializable data class ResetToFactory( val userWalletId: UserWalletId, - val cardSpecificInfo: CardSpecificInfo, - ) : AppRoute(path = "/reset_to_factory/${userWalletId.stringValue}/$cardSpecificInfo"), RouteBundleParams { + val cardId: String, + val isActiveBackupStatus: Boolean, + val backupCardsCount: Int, + ) : AppRoute( + path = "/reset_to_factory" + + "/${userWalletId.stringValue}" + + "/$cardId" + + "/$isActiveBackupStatus" + + "/$backupCardsCount", + ), + RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) - @Serializable - data class CardSpecificInfo(val cardId: String, val backupStatus: CardDTO.BackupStatus?) - companion object { const val USER_WALLET_ID = "userWalletId" - const val CARD_SPECIFIC_DATA = "cardSpecificInfo" + const val CARD_ID = "cardId" + const val IS_ACTIVE_BACKUP_STATUS = "isActiveBackupStatus" + const val BACKUP_CARDS_COUNT = "backupCardsCount" } } 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 6aa2933ba2..54fe4ff322 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 @@ -1,5 +1,6 @@ package com.tangem.datasource.local.userwallet +import com.tangem.common.CompletionResult import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -14,6 +15,8 @@ interface UserWalletsStore { suspend fun getAllSyncOrNull(): List? - @Throws - suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) + suspend fun update( + userWalletId: UserWalletId, + update: suspend (UserWallet) -> UserWallet, + ): CompletionResult } \ No newline at end of file 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 91ceeb0049..fcce746f21 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 @@ -8,8 +8,6 @@ import com.tangem.common.card.EncryptionMode import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.operations.attestation.Attestation -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.util.Date import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion @@ -300,19 +298,12 @@ data class CardDTO( } } - @Serializable sealed class BackupStatus { - @Serializable - @SerialName("card_linked") data class CardLinked(val cardCount: Int) : BackupStatus() - @Serializable - @SerialName("active") data class Active(val cardCount: Int) : BackupStatus() - @Serializable - @SerialName("no_backup") data object NoBackup : BackupStatus() val isActive: Boolean diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt index 9a9c5575d1..ec33cbbaaf 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt @@ -24,7 +24,7 @@ internal class DefaultDetailsComponent @AssistedInject constructor( private val model: DetailsModel = getOrCreateModel(params) private val userWalletListComponent = userWalletListComponentFactory.create( - context = child(key = "user_wallet_list"), + context = child(key = "user_wallet_list_component"), ) @Composable diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index 47ae7ed4f7..80a7f98ce1 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -1,17 +1,22 @@ package com.tangem.features.details.utils -import arrow.core.raise.* -import arrow.core.recover +import androidx.compose.ui.res.stringResource +import arrow.core.raise.Raise +import arrow.core.raise.ensureNotNull +import arrow.core.raise.fold +import arrow.core.raise.recover import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.navigation.popTo import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.SimpleOkDialog import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.ContentMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.models.scan.ScanResponse @@ -21,7 +26,7 @@ import com.tangem.domain.wallets.models.SaveWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.domain.wallets.usecase.SelectWalletUseCase +import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase import com.tangem.features.details.impl.R import javax.inject.Inject @@ -31,7 +36,7 @@ internal class UserWalletSaver @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val saveWalletUseCase: SaveWalletUseCase, private val generateWalletNameUseCase: GenerateWalletNameUseCase, - private val selectWalletUseCase: SelectWalletUseCase, + private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val reduxStateHolder: ReduxStateHolder, private val messageSender: UiMessageSender, private val router: Router, @@ -43,8 +48,6 @@ internal class UserWalletSaver @Inject constructor( val userWallet = createUserWallet(response) saveWallet(userWallet) - - router.popTo() }, recover = { error -> val message = error.message @@ -56,28 +59,44 @@ internal class UserWalletSaver @Inject constructor( ) private suspend fun Raise.saveWallet(userWallet: UserWallet) { - saveWalletUseCase(userWallet).recover { error -> - when (error) { - is SaveWalletError.WalletAlreadySaved -> selectUserWallet(userWallet) - is SaveWalletError.DataError -> { - val messageRef = ensureNotNull(error.messageId?.let(::resourceReference)) { - Error.Unknown + fold( + block = { saveWalletUseCase(userWallet).bind() }, + recover = { error -> + when (error) { + is SaveWalletError.WalletAlreadySaved -> { + if (shouldSaveUserWalletsSyncUseCase()) { + selectUserWallet() + } else { + router.popTo() + } } + is SaveWalletError.DataError -> { + val messageRef = ensureNotNull(error.messageId?.let(::resourceReference)) { + Error.Unknown + } - raise(Error.Message(messageRef)) + raise(Error.Message(messageRef)) + } } - } - }.bind() + }, + transform = { + // call only if wallet is successfully saved + reduxStateHolder.onUserWalletSelected(userWallet) - reduxStateHolder.onUserWalletSelected(userWallet) + router.popTo() + }, + ) } - private suspend fun Raise.selectUserWallet(userWallet: UserWallet) { - withError({ Error.Unknown }) { - selectWalletUseCase(userWallet.walletId).bind() - } - - router.popTo() + private fun selectUserWallet() { + messageSender.send( + message = ContentMessage { onDismiss -> + SimpleOkDialog( + message = stringResource(id = R.string.user_wallet_list_error_wallet_already_saved), + onDismissDialog = onDismiss, + ) + }, + ) } private suspend fun Raise.createUserWallet(response: ScanResponse): UserWallet { diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index b3740659cc..208feec00e 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -59,7 +59,11 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { .background(backgroundColor) .statusBarsPadding(), ) { - Column(modifier = Modifier.padding(bottom = bottomPadding)) { + Column( + modifier = Modifier + .padding(bottom = bottomPadding) + .fillMaxSize(), + ) { TangemTopAppBar( title = resourceReference(R.string.disclaimer_title), startButton = TopAppBarButtonUM( @@ -110,6 +114,7 @@ private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { it.setBackgroundColor(backgroundColor.toArgb()) }, client = remember { DisclaimerWebViewClient() }, + modifier = Modifier.fillMaxSize(), ) AnimatedVisibility( @@ -117,6 +122,9 @@ private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { label = "Loading state change animation", enter = fadeIn(), exit = fadeOut(), + modifier = Modifier + .fillMaxSize() + .background(backgroundColor), ) { Box( modifier = Modifier diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 9ea4aa95ee..378b55e62d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults -import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -32,6 +31,7 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.OkxPromoNotification +import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.event.EventEffect @@ -64,7 +64,16 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { val snackbarHostState = remember { SnackbarHostState() } Scaffold( topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + snackbarHost = { + TangemSnackbarHost( + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = bottomBarHeight + TangemTheme.dimens.spacing16, + ), + hostState = snackbarHostState, + ) + }, contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 9e02bbe2c8..a6bb2a02db 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -368,7 +368,9 @@ private fun BaseScaffoldWithMarkets( WalletSnackbarHost( snackbarHostState = it, event = state.event, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing4) + .navigationBarsPadding(), ) }, containerColor = TangemTheme.colors.background.secondary, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index a15817a145..3a9681b918 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -341,6 +341,8 @@ internal class WalletViewModel @Inject constructor( } private suspend fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) { + walletScreenContentLoader.cancel(action.deletedWalletId) + walletScreenContentLoader.load( userWallet = action.selectedWallet, clickIntents = clickIntents, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 377070533f..56d140ad13 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -254,7 +254,6 @@ reactive-network = { module = "com.github.pwittchen:reactivenetwork-rx2", versio walletConnectCore = { module = "com.walletconnect:android-core", version.ref = "walletConnectCore" } walletConnectWeb3 = { module = "com.walletconnect:web3wallet", version.ref = "walletConnectWeb3" } prettyLogger = { module = "com.orhanobut:logger", version.ref = "prettyLogger" } -sprClient = { module = "com.spr:messengerclient", version.ref = "spr-client" } chucker = { module = "com.github.chuckerteam.chucker:library", version.ref = "chucker" } chuckerStub = { module = "com.github.chuckerteam.chucker:library-no-op", version.ref = "chucker" } mlKit-barcodeScanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlKit-barcodeScanning" } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index d65c211b5d..1d659a2d09 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -65,9 +65,7 @@ private fun AppExtension.configureBuildTypes() { private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buildType: BuildType) { when (buildType) { - BuildType.Release, - BuildType.External, - -> { + BuildType.Release -> { isDebuggable = false isMinifyEnabled = false proguardFiles(extension.getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") @@ -76,6 +74,11 @@ private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buil isDebuggable = true isMinifyEnabled = false } + BuildType.External -> { + initWith(extension.buildTypes.getByName(BuildType.Release.id)) + matchingFallbacks.add(BuildType.Release.id) + signingConfig = extension.signingConfigs.getByName(BuildType.Debug.id) + } BuildType.Internal, BuildType.Mocked, -> { From dcc3bb6c976b101814347c6dca80e218c0bc62d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jul 2024 13:24:33 +0500 Subject: [PATCH 10/87] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- app/src/main/assets/testnet_tokens.json | 10 ++++++++++ .../tangem/datasource/config/ConfigManagerImpl.kt | 1 + .../tangem/datasource/config/models/JsonModels.kt | 1 + .../tangem/core/ui/extensions/BlockchainIcons.kt | 5 +++++ core/ui/src/main/res/drawable/ic_blast_22.xml | 12 ++++++++++++ core/ui/src/main/res/drawable/img_blast_22.xml | 15 +++++++++++++++ gradle/dependencies.toml | 2 +- .../converters/BlockchainSDKConfigConverter.kt | 1 + .../com/tangem/blockchainsdk/utils/Blockchain.kt | 5 +++++ 10 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_blast_22.xml create mode 100644 core/ui/src/main/res/drawable/img_blast_22.xml diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 35af912b96..faab90517c 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 35af912b96ce0f871906061bd483d9cf3af7451d +Subproject commit faab90517cc23bc700a0028146d84bec056f6d49 diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index 78da3c2c81..51a162df5c 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -688,6 +688,16 @@ "networkId": "base/test" } ] + }, + { + "id": "blast-ethereum", + "name": "Blast", + "symbol": "ETH", + "networks": [ + { + "networkId": "blast/test" + } + ] } ] } 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 36bba25d83..b2f7de050a 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 @@ -154,6 +154,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { zkSyncEra = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC), polygonZkEvm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC), base = GetBlockAccessToken(rest = accessTokens.base?.jsonRPC), + blast = GetBlockAccessToken(jsonRpc = accessTokens.blast?.jsonRPC), ) } } 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 a6929291e1..47ed736e3d 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 @@ -76,6 +76,7 @@ data class GetBlockAccessTokens( @Json(name = "polygon-zkevm") val polygonZkevm: GetBlockToken?, @Json(name = "zksync") val zksync: GetBlockToken?, @Json(name = "base") val base: GetBlockToken?, + @Json(name = "blast") val blast: GetBlockToken?, ) @JsonClass(generateAdapter = true) 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 33a8bd032e..1b7678e46d 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 @@ -70,6 +70,7 @@ fun getActiveIconRes(blockchainId: String): Int { "joystream" -> R.drawable.img_joystream_22 "koinos", "koinos/test" -> R.drawable.img_koinos_22 "bittensor" -> R.drawable.img_bittensor_22 + "blast", "blast/test" -> R.drawable.img_blast_22 else -> R.drawable.ic_alert_24 } } @@ -141,6 +142,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "joystream" -> R.drawable.img_joystream_22 "koinos", "koinos/test" -> R.drawable.img_koinos_22 "bittensor" -> R.drawable.img_bittensor_22 + "blast", "blast/test" -> R.drawable.img_blast_22 else -> R.drawable.ic_alert_24 } } @@ -209,6 +211,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "joystream" -> R.drawable.img_joystream_22 "koinos", "koinos/test" -> R.drawable.img_koinos_22 "bittensor" -> R.drawable.img_bittensor_22 + "blast", "blast/test" -> R.drawable.img_blast_22 else -> R.drawable.ic_alert_24 } } @@ -280,6 +283,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "joystream" -> R.drawable.ic_joystream_22 "koinos", "koinos/test" -> R.drawable.ic_koinos_22 "bittensor" -> R.drawable.ic_bittensor_22 + "blast", "blast/test" -> R.drawable.ic_blast_22 else -> R.drawable.ic_alert_24 } } @@ -351,6 +355,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "joystream" -> R.drawable.ic_joystream_22 "koinos", "koinos/test" -> R.drawable.ic_koinos_22 "bittensor" -> R.drawable.ic_bittensor_22 + "blast", "blast/test" -> R.drawable.ic_blast_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_blast_22.xml b/core/ui/src/main/res/drawable/ic_blast_22.xml new file mode 100644 index 0000000000..fea379b692 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_blast_22.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/img_blast_22.xml b/core/ui/src/main/res/drawable/img_blast_22.xml new file mode 100644 index 0000000000..515289963d --- /dev/null +++ b/core/ui/src/main/res/drawable/img_blast_22.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 56d140ad13..1a63543fe1 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-710" +tangemBlockchainSdk = "develop-711" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt index f8e769ebd1..9767d3d290 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt @@ -85,6 +85,7 @@ internal object BlockchainSDKConfigConverter : Converter Blockchain.Joystream "bittensor" -> Blockchain.Bittensor "filecoin" -> Blockchain.Filecoin + "blast" -> Blockchain.Blast + "blast/test" -> Blockchain.BlastTestnet else -> null } } @@ -239,6 +241,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.Joystream -> "joystream" Blockchain.Bittensor -> "bittensor" Blockchain.Filecoin -> "filecoin" + Blockchain.Blast -> "blast" + Blockchain.BlastTestnet -> "blast/test" } } @@ -316,6 +320,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Joystream -> "joystream" Blockchain.Bittensor -> "bittensor" Blockchain.Filecoin -> "filecoin" + Blockchain.Blast, Blockchain.BlastTestnet -> "blast" } } From d42a9d5b467a1e215490bbfbc9ca255e69176739 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jul 2024 12:24:03 +0500 Subject: [PATCH 11/87] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 28 ++++++++++--- .../state/InnerYieldBalanceState.kt | 12 ++---- .../converters/YieldBalancesConverter.kt | 41 ++++++++----------- .../previewdata/InitialStakingStatePreview.kt | 4 +- .../ui/StakingInitialInfoContent.kt | 6 ++- 5 files changed, 51 insertions(+), 40 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4172b1298d..46299409f1 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -13,6 +13,9 @@ 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. + Thank you for your feedback. We will respond as soon as possible + Your suggestions were sent + Please try to tap the card exactly as shown in the animation or read our simple guide, or request support. If the problem persists, please request support. Are you having difficulty scanning your card? This card is not designed to work with this app Default Fee @@ -28,6 +31,8 @@ Dark Light System default + If system is selected, the app will auto-adjust based on your device\'s system settings + System Theme App settings To hide or show your balances, simply flip your device screen down, or switch it off in Settings @@ -39,7 +44,7 @@ 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 - From your fiat card or bank account + With your bank card or bank account %d card %d cards @@ -95,6 +100,7 @@ Delete Disabled + Disconnect Done Enable Enabled @@ -110,6 +116,7 @@ Speed and fee Get addresses Go to provider + Go to token Import Later Locked @@ -130,6 +137,7 @@ Reject Reload Rename + Retry Save Save changes Search @@ -158,6 +166,7 @@ There was an error. Please try again. Unreachable Unstake + Warning Yes Contract address copied! Available networks @@ -205,6 +214,7 @@ Flip-to-Hide Balances Issuer Signed + If you forget the code you will lose access to your funds. Code recovery is not possible. Send feedback Details Check your internet connection or switch to a different network @@ -225,6 +235,8 @@ Exchange by %s Visit provider’s website to refund your money Operation failed by provider + The transaction amount was refunded in %1$s to your wallet due to OKX or bridge rules. %2$s + The amount was refunded in %1$s (%2$s network) Visit provider’s website for verification KYC verification required by provider Canceled @@ -264,7 +276,7 @@ No tokens found. Please try another request ID: %s Transaction ID copied - From another currency in your wallet + With another currency in your wallet 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 @@ -338,7 +350,7 @@ Available networks My portfolio Market - To generate addresses for selected networks, you need to attach a Tangem card + To generate addresses for selected networks, you must scan your Tangem Wallet card. Unable to load the data… Quick actions Result @@ -499,7 +511,7 @@ Camera access denied %1$s (%2$s) on %3$s network Send only %s to this address. Sending any other currency will result in its irreversible loss. - Show QR-code or share your address + Show a QR-code or share your address 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. @@ -565,6 +577,7 @@ %1$s, %2$s Address Destination Tag + Are you sure you want to close the send screen? Enter address Address is the same as wallet address Invalid Tag. It won\'t be added to the transaction. @@ -629,7 +642,7 @@ ≈ %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 - %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron(TRX) in your account. + %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account. Invalid address %1$s (%2$s) Transaction sent @@ -661,13 +674,15 @@ The period you must wait after requesting to withdraw funds from staking before the tokens become available. Warmup period The allocated time for activating participation in staking. + Stake %s Native staking Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days. Earn staking rewards Rewards Stake more - Unstacked + Unstaked Check unstaked to claim your assets + Unstaking Validator Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet @@ -709,6 +724,7 @@ Sending funds will be available once the pending transaction(s) in network %s is complete Selling %s is not available at the moment. Please check our updates. Staking %s is not available at the moment. Please check our updates. + Choose address 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. diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index dcea12e0ed..78ef0dc9dd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import kotlinx.collections.immutable.ImmutableList @@ -21,7 +22,8 @@ data class BalanceGroupedState( val items: ImmutableList, val footer: TextReference?, val title: TextReference, - val type: BalanceGroupType, + val type: BalanceType, + val isClickable: Boolean, ) data class BalanceState( @@ -33,10 +35,4 @@ data class BalanceState( val rawCurrencyId: String?, val unbondingPeriod: TextReference, val pendingActions: ImmutableList, -) - -enum class BalanceGroupType { - ACTIVE, - UNSTAKED, - UNKNOWN, -} \ No newline at end of file +) \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 739015eb13..4fb15fd144 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -10,7 +10,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.BalanceGroupType import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState @@ -59,12 +58,14 @@ internal class YieldBalancesConverter( .groupBy { it.type.toGroup() } .mapNotNull { item -> val (title, footer) = getGroupTitle(item.key) + val isClickable = getClickableType(item.key) title?.let { BalanceGroupedState( items = item.value.mapBalances().toPersistentList(), footer = footer, title = it, type = item.key, + isClickable = isClickable, ) } } @@ -111,31 +112,25 @@ internal class YieldBalancesConverter( } private fun BalanceType.toGroup() = when (this) { - BalanceType.PREPARING, - BalanceType.STAKED, - BalanceType.AVAILABLE, - BalanceType.LOCKED, - -> BalanceGroupType.ACTIVE - BalanceType.UNSTAKING, - BalanceType.UNLOCKING, - BalanceType.UNSTAKED, - -> BalanceGroupType.UNSTAKED BalanceType.REWARDS, BalanceType.UNKNOWN, - -> BalanceGroupType.UNKNOWN + -> BalanceType.UNKNOWN + else -> this } - private fun getGroupTitle(type: BalanceGroupType) = when (type) { - BalanceGroupType.ACTIVE -> resourceReference( - R.string.staking_active, - ) to resourceReference( - R.string.staking_active_footer, - ) - BalanceGroupType.UNSTAKED -> resourceReference( - R.string.staking_unstaked, - ) to resourceReference( - R.string.staking_unstaked_footer, - ) - BalanceGroupType.UNKNOWN -> null to null + private fun getGroupTitle(type: BalanceType) = when (type) { + BalanceType.STAKED -> resourceReference(R.string.staking_active) to + resourceReference(R.string.staking_active_footer) + BalanceType.UNSTAKED -> resourceReference(R.string.staking_unstaked) to + resourceReference(R.string.staking_unstaked_footer) + BalanceType.UNSTAKING -> resourceReference(R.string.staking_unstaking) to null + else -> null to null + } + + private fun getClickableType(type: BalanceType) = when (type) { + BalanceType.STAKED, + BalanceType.UNSTAKED, + -> true + else -> false } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 2255ba2015..2a5b57532b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.previewdata import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* @@ -68,7 +69,8 @@ internal object InitialStakingStatePreview { BalanceGroupedState( title = stringReference("Staked"), footer = null, - type = BalanceGroupType.ACTIVE, + type = BalanceType.STAKED, + isClickable = true, items = persistentListOf( BalanceState( cryptoValue = "100", diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index eae933e9b6..cd1f6aaac1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -31,6 +31,7 @@ import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview @@ -210,7 +211,7 @@ private fun ActiveStakingBlock(groups: List, onClick: (Bala ) { group.items.forEachIndexed { index, balance -> key(balance.validator.address) { - val caption = if (group.type == BalanceGroupType.UNSTAKED) { + val caption = if (group.type == BalanceType.UNSTAKING) { combinedReference( resourceReference(R.string.staking_details_unbonding_period), annotatedReference { @@ -240,13 +241,14 @@ private fun ActiveStakingBlock(groups: List, onClick: (Bala title = group.title.takeIf { index == 0 }, subtitle = stringReference(balance.validator.name), caption = caption, - isGrayscaleImage = group.type == BalanceGroupType.UNSTAKED, + isGrayscaleImage = group.type == BalanceType.UNSTAKING, infoTitle = balance.fiatAmount, infoSubtitle = balance.cryptoAmount, imageUrl = balance.validator.image.orEmpty(), modifier = Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), + enabled = group.isClickable, onClick = { onClick(balance) }, ), ) From a7b2779e9fa4135b898374bf810ab2135332486a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jul 2024 12:24:45 +0500 Subject: [PATCH 12/87] Updated on 2026-08-14 --- .../impl/presentation/state/StakingUiState.kt | 1 + .../converters/YieldBalancesConverter.kt | 16 +++++- .../state/stub/StakingClickIntentsStub.kt | 4 +- .../SetConfirmationStateAssentTransformer.kt | 2 + .../SetConfirmationStateLoadingTransformer.kt | 1 + .../ui/StakingClaimRewardsValidatorContent.kt | 2 +- .../viewmodel/StakingClickIntents.kt | 4 +- .../viewmodel/StakingViewModel.kt | 50 +++++++++++++------ gradle/dependencies.toml | 2 +- 9 files changed, 58 insertions(+), 24 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index ae02e3c71b..fe6ccd4ad7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -83,6 +83,7 @@ internal sealed class StakingStates { val notifications: ImmutableList, val footerText: String, val transactionDoneState: TransactionDoneState, + val pendingActionInProgress: PendingAction? = null, ) : ConfirmationState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 4fb15fd144..3411ac4946 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -124,13 +124,25 @@ internal class YieldBalancesConverter( BalanceType.UNSTAKED -> resourceReference(R.string.staking_unstaked) to resourceReference(R.string.staking_unstaked_footer) BalanceType.UNSTAKING -> resourceReference(R.string.staking_unstaking) to null - else -> null to null + BalanceType.AVAILABLE -> null to null + BalanceType.PREPARING -> null to null + BalanceType.REWARDS -> null to null + BalanceType.LOCKED -> null to null + BalanceType.UNLOCKING -> null to null + BalanceType.UNKNOWN -> null to null } private fun getClickableType(type: BalanceType) = when (type) { BalanceType.STAKED, BalanceType.UNSTAKED, -> true - else -> false + BalanceType.AVAILABLE, + BalanceType.UNSTAKING, + BalanceType.PREPARING, + BalanceType.REWARDS, + BalanceType.LOCKED, + BalanceType.UNLOCKING, + BalanceType.UNKNOWN, + -> false } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index b9a90741b4..8fd79c0d45 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -13,6 +13,8 @@ object StakingClickIntentsStub : StakingClickIntents { override fun onNextClick(pendingActions: ImmutableList) {} + override fun onActionClick(pendingAction: PendingAction?) {} + override fun onPrevClick() {} override fun onInfoClick(infoType: InfoType) {} @@ -33,8 +35,6 @@ object StakingClickIntentsStub : StakingClickIntents { override fun openRewardsValidators() {} - override fun selectRewardValidator(rewardValue: String) {} - override fun onExploreClick() {} override fun onShareClick() {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index f2dc22698e..6ebb7a8cc9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -19,6 +19,7 @@ internal class SetConfirmationStateAssentTransformer( private val cryptoCurrencyStatusProvider: Provider, private val stakingGasEstimate: StakingGasEstimate, private val pendingActionList: ImmutableList, + private val pendingAction: PendingAction?, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -48,6 +49,7 @@ internal class SetConfirmationStateAssentTransformer( ), validatorState = validatorState.copySealed(isClickable = true), pendingActions = pendingActionList, + pendingActionInProgress = pendingAction, isPrimaryButtonEnabled = true, ) } else { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index b9bd36aff7..88662f7fde 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -33,6 +33,7 @@ internal class SetConfirmationStateLoadingTransformer( footerText = "", transactionDoneState = TransactionDoneState.Empty, pendingActions = persistentListOf(), + pendingActionInProgress = null, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt index 64a2f51b56..6657db14f0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt @@ -63,7 +63,7 @@ internal fun StakingClaimRewardsValidatorContent( .background(TangemTheme.colors.background.action) .clickable( onClick = { - clickIntents.selectRewardValidator(item.cryptoValue) + clickIntents.onActiveStake(item) }, ), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 8302d36d14..9e71013bd6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -14,6 +14,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onNextClick(pendingActions: ImmutableList = persistentListOf()) + fun onActionClick(pendingAction: PendingAction?) + fun onPrevClick() fun onInfoClick(infoType: InfoType) @@ -26,8 +28,6 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun openRewardsValidators() - fun selectRewardValidator(rewardValue: String) - fun onActiveStake(activeStake: BalanceState) fun onExploreClick() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index c656944417..0b1ec36847 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -22,6 +22,7 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase @@ -62,6 +63,7 @@ internal class StakingViewModel @Inject constructor( private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, private val submitHashUseCase: SubmitHashUseCase, private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase, + private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -101,14 +103,18 @@ internal class StakingViewModel @Inject constructor( } override fun onNextClick(pendingActions: ImmutableList) { - handleOnNextConfirmationClick() stakingStateRouter.onNextClick() if (isAssentState()) { estimateGas(pendingActions) } } - private fun handleOnNextConfirmationClick() { + override fun onActionClick(pendingAction: PendingAction?) { + handleOnNextConfirmationClick(pendingAction) + stakingStateRouter.onNextClick() + } + + private fun handleOnNextConfirmationClick(pendingAction: PendingAction?) { if (isAssentState()) { viewModelScope.launch { stateController.update(SetConfirmationStateInProgressTransformer()) @@ -117,7 +123,6 @@ internal class StakingViewModel @Inject constructor( value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: error("No validator provided") - val pendingActions = confirmationState.pendingActions val stakingTransaction = getStakingTransactionUseCase( userWalletId = userWalletId, @@ -131,8 +136,8 @@ internal class StakingViewModel @Inject constructor( ?: error("No available address"), validatorAddress = validatorState.chosenValidator.address, token = yield.token, - passthrough = pendingActions.firstOrNull()?.passthrough, - type = pendingActions.firstOrNull()?.type, + passthrough = pendingAction?.passthrough, + type = pendingAction?.type, ), ).getOrElse { error(it) @@ -143,7 +148,8 @@ internal class StakingViewModel @Inject constructor( transactionId = stakingTransaction.id, gasEstimate = stakingTransaction.gasEstimate ?: error("No gas estimate available"), txData = TransactionData.Compiled(value = it.hexToBytes()), - pendingActions = pendingActions, + pendingActionList = confirmationState.pendingActions, + pendingAction = pendingAction, ) } ?: error("No unsigned transaction available") } @@ -159,6 +165,7 @@ internal class StakingViewModel @Inject constructor( ) val cryptoCurrencyValue = cryptoCurrencyStatus.value + val pendingAction = pendingActions.firstOrNull() val stakingGasEstimate = estimateGasUseCase( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, @@ -171,8 +178,8 @@ internal class StakingViewModel @Inject constructor( ?: error("No available address"), validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"), token = yield.token, - passthrough = pendingActions.firstOrNull()?.passthrough, - type = pendingActions.firstOrNull()?.type, + passthrough = pendingAction?.passthrough, + type = pendingAction?.type, ), ).getOrElse { error("Can't get fee info") } @@ -182,6 +189,7 @@ internal class StakingViewModel @Inject constructor( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, stakingGasEstimate = stakingGasEstimate, pendingActionList = pendingActions, + pendingAction = pendingAction, ), ) } @@ -226,11 +234,6 @@ internal class StakingViewModel @Inject constructor( onNextClick() } - override fun selectRewardValidator(rewardValue: String) { - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, rewardValue)) - onNextClick() - } - override fun onActiveStake(activeStake: BalanceState) { val routeType = if (activeStake.pendingActions.isEmpty()) { RouteType.UNSTAKE @@ -321,7 +324,8 @@ internal class StakingViewModel @Inject constructor( transactionId: String, gasEstimate: StakingGasEstimate, txData: TransactionData, - pendingActions: ImmutableList, + pendingActionList: ImmutableList, + pendingAction: PendingAction?, ) { sendTransactionUseCase( txData = txData, @@ -335,14 +339,15 @@ internal class StakingViewModel @Inject constructor( appCurrencyProvider = Provider { appCurrency }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, stakingGasEstimate = gasEstimate, - pendingActionList = pendingActions, + pendingActionList = pendingActionList, + pendingAction = pendingAction, ), ) // todo add error dialog }, ifRight = { txHash -> submitHash(transactionId, txHash) - + updateStakeBalance() val txUrl = getExplorerTransactionUrlUseCase( txHash = txHash, networkId = cryptoCurrencyStatus.currency.network.id, @@ -375,6 +380,19 @@ internal class StakingViewModel @Inject constructor( } } + private fun updateStakeBalance() { + viewModelScope.launch { + stakingYieldBalanceUseCase( + userWalletId = userWalletId, + address = CryptoCurrencyAddress( + cryptoCurrencyStatus.currency, + cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), + ), + refresh = true, + ) + } + } + private fun isAssentState(): Boolean { return value.currentStep == StakingStep.Confirmation && (value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState == diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 1a63543fe1..1c1542e3b1 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-711" +tangemBlockchainSdk = "develop-713" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 5210d338e437b5a2d0e69923ca38749865cb20cb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Aug 2024 12:05:59 +0500 Subject: [PATCH 13/87] Updated on 2026-08-14 --- .../ui/components/inputrow/InputRowEnter.kt | 2 +- .../inputrow/InputRowEnterAmount.kt | 2 +- .../components/inputrow/InputRowEnterInfo.kt | 2 +- .../inputrow/InputRowEnterInfoAmount.kt | 2 +- .../ui/components/inputrow/InputRowImage.kt | 2 +- .../components/inputrow/InputRowImageBase.kt | 16 ++-- .../components/inputrow/InputRowRecipient.kt | 2 +- .../inputrow/InputRowRecipientDefault.kt | 2 +- .../ui/recipient/SendRecipientContent.kt | 2 +- .../impl/presentation/ui/send/FeeBlock.kt | 2 +- .../presentation/ui/send/RecipientBlock.kt | 2 +- .../presentation/ui/block/StakingFeeBlock.kt | 2 +- .../swap/preview/FeeItemStatePreview.kt | 20 +++++ .../com/tangem/feature/swap/ui/FeeItem.kt | 79 ++++++++----------- .../tangem/feature/swap/ui/ProviderItem.kt | 2 +- 15 files changed, 73 insertions(+), 66 deletions(-) create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/preview/FeeItemStatePreview.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt index 4d1490891f..6a1b07ab53 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt @@ -72,7 +72,7 @@ fun InputRowEnter( Column(modifier = Modifier.weight(1f)) { Text( text = title.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = titleColor, ) SimpleTextField( 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 28c9082f90..a4759a11e2 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 @@ -72,7 +72,7 @@ fun InputRowEnterAmount( Column(modifier = Modifier.weight(1f)) { Text( text = title.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = titleColor, ) AmountTextField( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt index c9e01fd85f..cde9e9b0dd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt @@ -62,7 +62,7 @@ fun InputRowEnterInfo( ) { Text( text = title.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = titleColor, ) Row { 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 f3d8a0c1cc..bf0e7bb5ed 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 @@ -70,7 +70,7 @@ fun InputRowEnterInfoAmount( ) { Text( text = title.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = titleColor, ) Row { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt index 58a61bb00e..d816d77bf6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt @@ -70,7 +70,7 @@ fun InputRowImage( ) { Text( text = title.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = titleColor, ) Row( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt index 417a7edbec..9af3516af2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt @@ -15,7 +15,7 @@ import com.tangem.core.ui.res.TangemTheme @Composable internal fun InputRowImageBase( subtitle: TextReference, - caption: TextReference, + caption: TextReference?, imageUrl: String, modifier: Modifier = Modifier, subtitleColor: Color = TangemTheme.colors.text.primary1, @@ -40,12 +40,14 @@ internal fun InputRowImageBase( style = TangemTheme.typography.subtitle2, color = subtitleColor, ) - Text( - text = caption.resolveAnnotatedReference(), - style = TangemTheme.typography.caption2, - color = captionColor, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing2), - ) + if (caption != null) { + Text( + text = caption.resolveAnnotatedReference(), + style = TangemTheme.typography.caption2, + color = captionColor, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing2), + ) + } } extraContent() } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt index b1df3b3e40..8d5363d3cc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt @@ -75,7 +75,7 @@ fun InputRowRecipient( AnimatedContent(targetState = titleText, label = "Title Change") { Text( text = it.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = color, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt index 94f75c6a56..f9941fcb9a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt @@ -50,7 +50,7 @@ fun InputRowRecipientDefault( ) { Text( text = title.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = titleColor, ) Row( 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 cf63542cee..75729e23b1 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 @@ -141,7 +141,7 @@ private fun LazyListScope.memoField(memoField: SendTextField.RecipientMemo?, onM onValueChange = memoField.onValueChange, onPasteClick = onMemoChange, modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), - labelStyle = TangemTheme.typography.caption2, + labelStyle = TangemTheme.typography.subtitle2, isError = memoField.isError, error = memoField.error, isReadOnly = !memoField.isEnabled, 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 0339ddc783..1c8e735b07 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 @@ -39,7 +39,7 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, o ) { Text( text = stringResource(R.string.common_network_fee_title), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.secondary, ) 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 4839b83690..313451a067 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 @@ -52,7 +52,7 @@ internal fun RecipientBlock( private fun AddressBlock(address: SendTextField.RecipientAddress) { Text( text = address.label.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.secondary, ) Row( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index 4dff5385fb..c552ca2fbc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -39,7 +39,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { ) { Text( text = stringResource(R.string.common_network_fee_title), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.secondary, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/preview/FeeItemStatePreview.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/preview/FeeItemStatePreview.kt new file mode 100644 index 0000000000..4d066c82d2 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/preview/FeeItemStatePreview.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.swap.preview + +import com.tangem.core.ui.extensions.stringReference +import com.tangem.feature.swap.domain.models.ui.FeeType +import com.tangem.feature.swap.models.states.FeeItemState + +object FeeItemStatePreview { + + val state = FeeItemState.Content( + feeType = FeeType.NORMAL, + title = stringReference("Fee"), + amountCrypto = "1000", + symbolCrypto = "MATIC", + amountFiatFormatted = "(1000$)", + isClickable = false, + onClick = {}, + ) + + val stateClickable = state.copy(isClickable = true) +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt index e4c3b974ca..b3cb10ce22 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt @@ -1,20 +1,21 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH24 -import com.tangem.core.ui.components.rows.SimpleActionRow -import com.tangem.core.ui.extensions.resolveReference +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.FeeItemState +import com.tangem.feature.swap.presentation.R +import com.tangem.feature.swap.preview.FeeItemStatePreview @Composable fun FeeItemBlock(state: FeeItemState) { @@ -25,53 +26,37 @@ fun FeeItemBlock(state: FeeItemState) { @Composable fun FeeItem(state: FeeItemState.Content) { - Box( + val description = "${state.amountCrypto} ${state.symbolCrypto} (${state.amountFiatFormatted})" + val icon = R.drawable.ic_chevron_right_24.takeIf { state.isClickable } + InputRowDefault( + title = state.title, + text = stringReference(description), + iconRes = icon, modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background(color = TangemTheme.colors.background.action) .clickable( + enabled = state.isClickable, onClick = state.onClick, - ) - .fillMaxWidth() - .defaultMinSize(minHeight = TangemTheme.dimens.size68), - ) { - val description = "${state.amountCrypto} ${state.symbolCrypto} (${state.amountFiatFormatted})" - SimpleActionRow( - modifier = Modifier.padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing12, ), - title = state.title.resolveReference(), - description = description, - isClickable = state.isClickable, - ) + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun FeeItem_Preview(@PreviewParameter(FeeItemPreviewProvider::class) data: FeeItemState.Content) { + TangemThemePreview { + FeeItem(data) } } -@Preview -@Composable -private fun FeeItemPreview() { - val state = FeeItemState.Content( - feeType = FeeType.NORMAL, - title = stringReference("Fee"), - amountCrypto = "1000", - symbolCrypto = "MATIC", - amountFiatFormatted = "(1000$)", - isClickable = false, - onClick = {}, - ) - Column { - TangemThemePreview(isDark = false) { - FeeItem(state = state) - } - - SpacerH24() - - TangemThemePreview(isDark = true) { - FeeItem(state = state) - } - } -} \ No newline at end of file +private class FeeItemPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + FeeItemStatePreview.state, + FeeItemStatePreview.state.copy(isClickable = true), + ) +} +// endregion \ No newline at end of file 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 315c7a064a..9bf6689b1c 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 @@ -263,7 +263,7 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) { Column { Text( text = stringResource(R.string.express_provider), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.secondary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), ) From c4adfe3d8998a55df36b7eb76ac9aac5fc23068e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Aug 2024 13:11:43 +0500 Subject: [PATCH 14/87] Updated on 2026-08-14 --- .../NavigationButtonsBlock.kt | 202 ++++++++++++++++ .../NavigationButtonsState.kt | 27 +++ .../preview/NavigationButtonsPreview.kt | 67 ++++++ core/res/src/main/res/values/strings.xml | 71 ++++++ .../common/TangemButtonIconPosition.kt | 2 +- .../data/staking/DefaultStakingRepository.kt | 8 +- .../action/StakingActionCommonType.kt | 3 +- .../state/StakingStateController.kt | 11 +- .../presentation/state/StakingStateRouter.kt | 13 +- .../impl/presentation/state/StakingUiState.kt | 12 +- .../SetButtonsStateTransformer.kt | 226 ++++++++++++++++++ .../SetConfirmationStateAssentTransformer.kt | 2 - ...tConfirmationStateInProgressTransformer.kt | 6 +- .../ui/StakingConfirmationContent.kt | 8 +- .../ui/StakingNavigationButtons.kt | 199 --------------- .../impl/presentation/ui/StakingScreen.kt | 12 +- .../viewmodel/StakingViewModel.kt | 30 +-- 17 files changed, 650 insertions(+), 249 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt new file mode 100644 index 0000000000..8a9ccffd1e --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -0,0 +1,202 @@ +package com.tangem.common.ui.navigationButtons + +import android.content.res.Configuration +import androidx.compose.animation.* +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.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.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview +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.TextReference +import com.tangem.core.ui.extensions.rememberHapticFeedback +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun NavigationButtonsBlock(buttonState: NavigationButtonsState, modifier: Modifier = Modifier) { + val state = buttonState as? NavigationButtonsState.Data + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.fillMaxWidth(), + ) { + ExtraButtons(state?.extraButtons, state?.txUrl) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + PreviousButton(state?.prevButton) + PrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f)) + } + + SecondaryButton(state?.secondaryButton) + } +} + +@Composable +private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = primaryButton, + transitionSpec = { + val isPrimaryToHide = targetState != null && initialState == null + val isPrimaryWasVisible = targetState == null && initialState != null + if (isPrimaryToHide || isPrimaryWasVisible) { + slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()) + .togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut())) + } else { + fadeIn().togetherWith(fadeOut()) + } + }, + contentAlignment = Alignment.Center, + label = "Animate show primary button", + modifier = modifier.fillMaxWidth(), + ) { button -> + if (button != null && button.textReference != TextReference.EMPTY) { + val icon = if (button.iconRes != null && button.isIconVisible) { + TangemButtonIconPosition.End(iconResId = button.iconRes) + } else { + TangemButtonIconPosition.None + } + TangemButton( + text = button.textReference.resolveReference(), + enabled = button.isEnabled, + onClick = button.onClick, + showProgress = button.showProgress, + colors = TangemButtonsDefaults.primaryButtonColors, + icon = icon, + modifier = Modifier.fillMaxWidth(), + ) + } else { + Spacer(modifier = Modifier.fillMaxWidth()) + } + } +} + +@Composable +private fun SecondaryButton(secondaryButton: NavigationButton?) { + AnimatedContent( + targetState = secondaryButton, + transitionSpec = { + val isPrimaryToHide = targetState != null && initialState == null + val isPrimaryWasVisible = targetState == null && initialState != null + if (isPrimaryToHide || isPrimaryWasVisible) { + slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()) + .togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut())) + } else { + fadeIn().togetherWith(fadeOut()) + } + }, + contentAlignment = Alignment.Center, + label = "Animate show secondary button", + modifier = Modifier.fillMaxWidth(), + ) { button -> + if (button != null && button.textReference != TextReference.EMPTY) { + val icon = button.iconRes?.let { TangemButtonIconPosition.End(iconResId = it) } + ?: TangemButtonIconPosition.None + + TangemButton( + text = button.textReference.resolveReference(), + enabled = button.isEnabled, + onClick = button.onClick, + icon = icon, + showProgress = button.showProgress, + colors = TangemButtonsDefaults.secondaryButtonColors, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + ) + } else { + Spacer(modifier = Modifier.fillMaxWidth()) + } + } +} + +@Composable +private fun PreviousButton(prevButton: NavigationButton?) { + AnimatedVisibility( + visible = prevButton != null, + enter = expandHorizontally(expandFrom = Alignment.End), + exit = shrinkHorizontally(shrinkTowards = Alignment.End), + label = "Animate show prev button", + ) { + val button = remember(this) { requireNotNull(prevButton) } + if (button.iconRes != null && button.isIconVisible) { + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(button.iconRes), + ), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.button.secondary) + .clickable(onClick = button.onClick) + .padding(TangemTheme.dimens.spacing12), + ) + } + } +} + +@Composable +private fun ExtraButtons(extraButtons: ImmutableList?, txUrl: String?) { + AnimatedVisibility( + visible = !txUrl.isNullOrBlank() && extraButtons != null, + enter = slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()), + exit = slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()), + label = "Animate show sent state buttons", + modifier = Modifier.fillMaxWidth(), + ) { + val buttons = remember(this) { requireNotNull(extraButtons) } + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + ) { + buttons.forEach { button -> + val icon = button.iconRes?.let { TangemButtonIconPosition.Start(iconResId = it) } + ?: TangemButtonIconPosition.None + TangemButton( + text = button.textReference.resolveReference(), + icon = icon, + onClick = rememberHapticFeedback(state = button, onAction = button.onClick), + modifier = Modifier.weight(1f), + enabled = button.isEnabled, + showProgress = false, + colors = TangemButtonsDefaults.secondaryButtonColors, + ) + } + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun NavigationButtonsBlock_Preview( + @PreviewParameter(NavigationButtonsBlockDataProvider::class) navigationButtonsState: NavigationButtonsState, +) { + TangemThemePreview { + NavigationButtonsBlock(navigationButtonsState) + } +} + +private class NavigationButtonsBlockDataProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(NavigationButtonsPreview.allButtons) +} + +// endregion \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt new file mode 100644 index 0000000000..c125a871d1 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt @@ -0,0 +1,27 @@ +package com.tangem.common.ui.navigationButtons + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +sealed class NavigationButtonsState { + data object Empty : NavigationButtonsState() + + data class Data( + val primaryButton: NavigationButton, + val prevButton: NavigationButton?, + val secondaryButton: NavigationButton?, + val extraButtons: ImmutableList, + val txUrl: String? = null, + ) : NavigationButtonsState() +} + +data class NavigationButton( + val textReference: TextReference, + @DrawableRes val iconRes: Int? = null, + val isSecondary: Boolean, + val isIconVisible: Boolean, + val showProgress: Boolean, + val isEnabled: Boolean, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt new file mode 100644 index 0000000000..738598de82 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt @@ -0,0 +1,67 @@ +package com.tangem.common.ui.navigationButtons.preview + +import com.tangem.common.ui.R +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationButtonsState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import kotlinx.collections.immutable.persistentListOf + +internal object NavigationButtonsPreview { + + private val extraButtons = persistentListOf( + NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_tangem_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = {}, + ), + NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_tangem_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = {}, + ), + ) + + private val next = NavigationButton( + textReference = resourceReference(R.string.common_next), + isSecondary = false, + isIconVisible = false, + showProgress = false, + isEnabled = true, + onClick = {}, + ) + private val prev = NavigationButton( + textReference = TextReference.EMPTY, + iconRes = R.drawable.ic_back_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = {}, + ) + + private val finished = NavigationButton( + textReference = resourceReference(R.string.common_close), + isSecondary = false, + isIconVisible = false, + showProgress = false, + isEnabled = true, + onClick = {}, + ) + + val allButtons = NavigationButtonsState.Data( + primaryButton = finished, + prevButton = prev, + secondaryButton = next, + extraButtons = extraButtons, + txUrl = "https://tangem.com", + ) +} \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 46299409f1..aaba92efbb 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -4,10 +4,13 @@ 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 How to scan Request support Try again 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 @@ -35,6 +38,8 @@ System Theme App settings + Go to settings to enable biometric authentication in the Tangem app + Enable biometric authentication To hide or show your balances, simply flip your device screen down, or switch it off in Settings Don\'t show again Got it @@ -43,6 +48,7 @@ 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. + Touch ID is used to save your cards in the app Start backup process With your bank card or bank account @@ -51,6 +57,7 @@ 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? @@ -79,13 +86,16 @@ Approval Approve Attention + Back Balance: %s Balance biometric authentication biometrics Buy Go to %1$s + Settings You have not given access to your camera, please adjust your privacy settings + Camera access denied Cancel Claim rewards Close @@ -119,6 +129,7 @@ Go to token Import Later + Learn & Earn Locked Main network Network fee @@ -131,6 +142,7 @@ Primary Card Passphrase Paste + Push %1$s-%2$s Read more Receive @@ -172,7 +184,9 @@ Available networks Add token Contract address + Please fill in all the fields Contract address is invalid + Derivation path is invalid Please select the network Decimal must be a valid integer, up to %li Custom derivation @@ -202,6 +216,7 @@ 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. + Long Tap Passcode Before executing any command entailing a change of the card state, you will have to enter the passcode. Referral program @@ -210,6 +225,7 @@ Card ID Contact support Link More Cards + You can synchronize up to three cards into one wallet. It can only be done once. App Currency Flip-to-Hide Balances Issuer @@ -224,6 +240,7 @@ 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 @@ -287,6 +304,7 @@ Feedback Tangem feedback Can\'t send a transaction + Can\'t push a transaction Current transaction The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. Specify the approve limit for the selected token @@ -301,8 +319,10 @@ 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 the card #%s of the wallet 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 @@ -314,6 +334,8 @@ Mana limit The Koinos network requires Mana for network fees. Your have %1$s/%2$s Mana Mana level + Please, set up an account to send email + No Mail accounts To begin tracking your crypto assets and transactions, add tokens Manage tokens To access all the networks you need to scan the card @@ -336,6 +358,11 @@ %1$d of %2$d wallet %1$d of %2$d wallets + %d of %#@total_wallets@ + + %d wallet + %d wallets + Remove e.g. BTC I trust, hodl I must Your portfolio has been updated @@ -427,6 +454,7 @@ Activation error Add tokens 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? + iPhone 7/7+ is not able to create a backup for Tangem Wallet due to some system limitations. Please use another phone to perform this operation. All other functions work stably. 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 @@ -451,6 +479,7 @@ Do you want to exit the activation process? Getting started 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. + Save your wallet Creating a backup Read more about seed phrase @@ -470,6 +499,7 @@ Invalid seed phrase. Please check the word order. Invalid seed phrase. Please check your spelling. Legacy + We do not recommend storing the seed phrase as a screenshot due to the high risk of loss or hacking To check whether you’ve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words So, let’s check To start the backup process add up to two backup cards. @@ -505,6 +535,9 @@ By balance Organize tokens Ungroup + Additional fee + Previous fee + Previous transaction total including fee Select from the gallery Settings You have not given access to your camera @@ -515,6 +548,7 @@ 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. + Your participation request could not be processed. Error code: %s. Please try again later. If the problem persists — feel free to contact our support. Upcoming payments Your friends bought Less @@ -550,11 +584,15 @@ 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 %s Allow to use biometrics + %s will be requested instead of the access code for interactions with your wallet Biometrics will be requested instead of the access code for interactions with your wallet Access code + Don\'t allow It looks like you have biometric authentication disabled, it is necessary to save wallets Enable biometric authorization + Would you like to use %s? Would you like to use biometrics? Note that making a transaction with your funds will still require your card Scan Card @@ -590,6 +628,7 @@ Priority Check your network connection Network fee info unreachable + From **%s** From Gas limit 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. @@ -653,6 +692,7 @@ Active To unstake your assets, click here. The amount to stake must be at least %s + Claim unstaked APR APY The annual percentage return you can earn from participating in staking. @@ -675,15 +715,26 @@ Warmup period The allocated time for activating participation in staking. Stake %s + Migrate Native staking Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days. Earn staking rewards + Rebond + Restake + Restake rewards + Revoke + Revote Rewards + Stake locked Stake more + Unlock locked Unstaked Check unstaked to claim your assets Unstaking Validator + Vote + Vote locked + Withdraw Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet Up to 3 physical cards to one wallet @@ -692,6 +743,8 @@ 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 @@ -707,6 +760,7 @@ Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Give Permission + Permit and Swap In progress Swap You receive @@ -772,6 +826,13 @@ Rename Wallet Unlock all Unlock all with %s + Close network fee settings + Nothing to paste from clipboard + Open card details + Open network fee settings + Scan QR code to open new WalletConnect session + Paste address from clipboard + Scan, QR, code with address Blockchain is unreachable. Try later Scan the card Requesting to sign a message.\n\n%s @@ -850,6 +911,8 @@ This token must be associated with your Hedera account before you can receive it Associate your token Not enough %s. Top up your Hedera account to associate this token + iPhone 7/7+ cannot sign transactions on this network. To complete this operation, please use a different phone. + Transaction signing unavailable 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. @@ -865,6 +928,10 @@ 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 + Funds in Tangem cards issued before September 2019 cannot be retrieved on iPhones due to iOS restrictions. Please use an Android phone for retrieval. Cards issued after September 2019 work correctly on both OS. + iOS restriction for older cards + Some iPhone 7/7+ models may have NFC issues during certain operations. + Device incompatibility detected Your review keeps us motivated to make Tangem Wallet even better Enjoying Tangem? You must associate your token before receiving tokens @@ -876,6 +943,10 @@ 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 + System update required + Support for your version of the operating system will end on %s. To receive future app updates you must update it to the latest version. + Tangem recommends installing the latest iOS update for stable and safe operation + System update available This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. For testing purposes only Discard diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonIconPosition.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonIconPosition.kt index 2c3821e659..0e6169baa3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonIconPosition.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonIconPosition.kt @@ -11,7 +11,7 @@ sealed interface TangemButtonIconPosition { data class End(@DrawableRes override val iconResId: Int) : TangemButtonIconPosition - object None : TangemButtonIconPosition { + data object None : TangemButtonIconPosition { @DrawableRes override val iconResId: Int? = null } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 1a2bc1999c..a8c7009e7b 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -187,7 +187,9 @@ internal class DefaultStakingRepository( params, ), ) - StakingActionCommonType.PENDING -> stakeKitApi.createPendingAction( + StakingActionCommonType.PENDING_OTHER, + StakingActionCommonType.PENDING_REWARDS, + -> stakeKitApi.createPendingAction( createPendingActionRequestBody(params), ) } @@ -217,7 +219,9 @@ internal class DefaultStakingRepository( params, ), ) - StakingActionCommonType.PENDING -> stakeKitApi.estimateGasOnPending( + StakingActionCommonType.PENDING_REWARDS, + StakingActionCommonType.PENDING_OTHER, + -> stakeKitApi.estimateGasOnPending( createPendingActionRequestBody(params), ) } diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt index 1caf4b8942..5a59ba211c 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt @@ -3,5 +3,6 @@ package com.tangem.domain.staking.model.stakekit.action enum class StakingActionCommonType { ENTER, EXIT, - PENDING, + PENDING_REWARDS, + PENDING_OTHER, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index d3570e6c23..2dd8aa4900 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -1,8 +1,11 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.ui.event.consumedEvent +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub +import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer import com.tangem.utils.transformer.Transformer import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -20,16 +23,21 @@ internal class StakingStateController @Inject constructor() { val uiState: StateFlow get() = mutableUiState.asStateFlow() + private val buttonsTransformer = SetButtonsStateTransformer() + fun update(function: (StakingUiState) -> StakingUiState) { mutableUiState.update(function = function) + mutableUiState.update(function = buttonsTransformer::transform) } fun update(transformer: Transformer) { mutableUiState.update(function = transformer::transform) + mutableUiState.update(function = buttonsTransformer::transform) } fun clear() { mutableUiState.update { getInitialState() } + mutableUiState.update(function = buttonsTransformer::transform) } private fun getInitialState(): StakingUiState { @@ -44,7 +52,8 @@ internal class StakingStateController @Inject constructor() { isBalanceHidden = false, event = consumedEvent(), bottomSheetConfig = null, - routeType = RouteType.STAKE, + actionType = StakingActionCommonType.ENTER, + buttonsState = NavigationButtonsState.Empty, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index d0485b1812..03cc906908 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.common.routing.AppRouter +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType internal class StakingStateRouter( private val appRouter: AppRouter, @@ -14,12 +15,12 @@ internal class StakingStateRouter( fun onNextClick() { when (stateController.value.currentStep) { - StakingStep.InitialInfo -> when (stateController.value.routeType) { - RouteType.STAKE -> showAmount() - RouteType.OTHER, - RouteType.UNSTAKE, + StakingStep.InitialInfo -> when (stateController.value.actionType) { + StakingActionCommonType.ENTER -> showAmount() + StakingActionCommonType.PENDING_OTHER, + StakingActionCommonType.EXIT, -> showConfirmation() - RouteType.CLAIM -> showRewardsValidators() + StakingActionCommonType.PENDING_REWARDS -> showRewardsValidators() } StakingStep.RewardsValidators, StakingStep.Validators, @@ -37,7 +38,7 @@ internal class StakingStateRouter( StakingStep.InitialInfo -> onBackClick() StakingStep.Amount -> showInitial() StakingStep.Confirmation -> { - if (uiState.routeType == RouteType.OTHER || uiState.routeType == RouteType.UNSTAKE) { + if (uiState.actionType != StakingActionCommonType.ENTER) { showInitial() } else { showAmount() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index fe6ccd4ad7..95f3d4cdab 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -2,11 +2,13 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList @@ -25,7 +27,8 @@ internal data class StakingUiState( val confirmationState: StakingStates.ConfirmationState, val isBalanceHidden: Boolean, val bottomSheetConfig: TangemBottomSheetConfig?, - val routeType: RouteType, + val actionType: StakingActionCommonType, + val buttonsState: NavigationButtonsState, val event: StateEvent, ) { @@ -98,11 +101,4 @@ enum class StakingStep { Amount, Confirmation, Validators, -} - -enum class RouteType { - STAKE, - UNSTAKE, - CLAIM, - OTHER, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt new file mode 100644 index 0000000000..9c9d86346f --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -0,0 +1,226 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationButtonsState +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal class SetButtonsStateTransformer : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + + val buttonsState = if (prevState.isButtonsVisible()) { + NavigationButtonsState.Data( + primaryButton = getPrimaryButton(prevState), + prevButton = getPrevButton(prevState), + secondaryButton = getSecondaryButton(prevState), + extraButtons = getExtraButtons(prevState), + txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl, + ) + } else { + NavigationButtonsState.Empty + } + + return prevState.copy(buttonsState = buttonsState) + } + + private fun getPrimaryButton(prevState: StakingUiState): NavigationButton { + val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val innerConfirmState = confirmState?.innerState + + val isPrimaryInProgress = + confirmState?.pendingActions?.getPrimaryAction() == confirmState?.pendingActionInProgress + val isConfirmation = prevState.currentStep == StakingStep.Confirmation + val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS + val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED + + val isIconVisible = isConfirmation && !isCompleted + val isShowProgress = isInProgress && isPrimaryInProgress + return NavigationButton( + textReference = prevState.getButtonText(), + iconRes = R.drawable.ic_tangem_24, + isSecondary = false, + isIconVisible = isIconVisible, + showProgress = isShowProgress, + isEnabled = prevState.isButtonEnabled(), + onClick = { prevState.onPrimaryClick() }, + ) + } + + private fun getSecondaryButton(prevState: StakingUiState): NavigationButton? { + val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val innerConfirmState = confirmState?.innerState + + val isConfirmation = prevState.currentStep == StakingStep.Confirmation + val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS + val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED + + return confirmState?.pendingActions?.getSecondaryAction()?.let { pendingAction -> + val isSecondaryInProgress = pendingAction == confirmState.pendingActionInProgress + val isShowProgress = isInProgress && isSecondaryInProgress + NavigationButton( + textReference = getPendingActionTitle(pendingAction.type), + iconRes = R.drawable.ic_tangem_24, + isSecondary = true, + isIconVisible = true, + showProgress = isShowProgress, + isEnabled = prevState.isButtonEnabled(), + onClick = { prevState.clickIntents.onActionClick(pendingAction) }, + ).takeIf { isConfirmation && !isCompleted } + } + } + + private fun getPrevButton(prevState: StakingUiState): NavigationButton? { + return NavigationButton( + textReference = TextReference.EMPTY, + iconRes = R.drawable.ic_back_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = prevState.clickIntents::onPrevClick, + ).takeIf { prevState.currentStep.isPrevButtonVisible() } + } + + private fun getExtraButtons(prevState: StakingUiState): ImmutableList { + return persistentListOf( + NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_web_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = prevState.clickIntents::onExploreClick, + ), + NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_share_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = prevState.clickIntents::onShareClick, + ), + ) + } + + private fun List.getPrimaryAction(): PendingAction? = getOrNull(0) + + private fun List.getSecondaryAction(): PendingAction? = getOrNull(1) + + private fun StakingUiState.isButtonsVisible(): Boolean = when (currentStep) { + StakingStep.InitialInfo -> { + val initialState = initialInfoState as? StakingStates.InitialInfoState.Data + initialState?.isStakeMoreAvailable == true || initialState?.yieldBalance is InnerYieldBalanceState.Empty + } + StakingStep.RewardsValidators -> false + else -> true + } + + private fun StakingUiState.getButtonText(): TextReference { + return when (currentStep) { + StakingStep.InitialInfo -> { + val initialState = initialInfoState as? StakingStates.InitialInfoState.Data + if (initialState?.yieldBalance is InnerYieldBalanceState.Data) { + resourceReference(R.string.staking_stake_more) + } else { + resourceReference(R.string.common_next) + } + } + + StakingStep.Confirmation -> { + if (confirmationState is StakingStates.ConfirmationState.Data) { + if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { + resourceReference(R.string.common_close) + } else { + when (actionType) { + StakingActionCommonType.ENTER -> resourceReference(R.string.common_stake) + StakingActionCommonType.EXIT -> resourceReference(R.string.common_unstake) + StakingActionCommonType.PENDING_OTHER, + StakingActionCommonType.PENDING_REWARDS, + -> getPendingActionTitle(confirmationState.pendingActions.firstOrNull()?.type) + } + } + } else { + resourceReference(R.string.common_close) + } + } + StakingStep.Validators -> resourceReference(R.string.common_continue) + StakingStep.Amount, + StakingStep.RewardsValidators, + -> resourceReference(R.string.common_next) + } + } + + private fun StakingUiState.onPrimaryClick() { + when (currentStep) { + StakingStep.InitialInfo, + StakingStep.Validators, + StakingStep.Amount, + -> clickIntents.onNextClick() + StakingStep.Confirmation -> { + if (confirmationState is StakingStates.ConfirmationState.Data) { + if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { + clickIntents.onBackClick() + } else { + clickIntents.onActionClick(confirmationState.pendingActions.firstOrNull()) + } + } else { + clickIntents.onBackClick() + } + } + StakingStep.RewardsValidators -> Unit + } + } + + private fun StakingStep.isPrevButtonVisible(): Boolean = when (this) { + StakingStep.InitialInfo, + StakingStep.RewardsValidators, + StakingStep.Confirmation, + -> false + StakingStep.Amount, + StakingStep.Validators, + -> true + } + + private fun StakingUiState.isButtonEnabled(): Boolean { + return when (currentStep) { + StakingStep.InitialInfo -> initialInfoState.isPrimaryButtonEnabled + StakingStep.Amount -> amountState.isPrimaryButtonEnabled + StakingStep.Confirmation -> confirmationState.isPrimaryButtonEnabled + StakingStep.RewardsValidators -> rewardsValidatorsState.isPrimaryButtonEnabled + StakingStep.Validators -> true + } + } + + @Suppress("CyclomaticComplexMethod") + private fun getPendingActionTitle(type: StakingActionType?): TextReference = when (type) { + StakingActionType.CLAIM_REWARDS -> resourceReference(R.string.common_claim_rewards) + StakingActionType.RESTAKE_REWARDS -> resourceReference(R.string.staking_restake_rewards) + StakingActionType.WITHDRAW -> resourceReference(R.string.staking_withdraw) + StakingActionType.RESTAKE -> resourceReference(R.string.staking_restake) + StakingActionType.CLAIM_UNSTAKED -> resourceReference(R.string.staking_claim_unstaked) + StakingActionType.UNLOCK_LOCKED -> resourceReference(R.string.staking_unlocked_locked) + StakingActionType.STAKE_LOCKED -> resourceReference(R.string.staking_stake_locked) + StakingActionType.VOTE -> resourceReference(R.string.staking_vote) + StakingActionType.REVOKE -> resourceReference(R.string.staking_revoke) + StakingActionType.VOTE_LOCKED -> resourceReference(R.string.staking_vote_locked) + StakingActionType.REVOTE -> resourceReference(R.string.staking_revote) + StakingActionType.REBOND -> resourceReference(R.string.staking_rebond) + StakingActionType.MIGRATE -> resourceReference(R.string.staking_migrate) + StakingActionType.STAKE -> resourceReference(R.string.common_stake) + StakingActionType.UNSTAKE -> resourceReference(R.string.common_unstake) + StakingActionType.UNKNOWN -> TextReference.EMPTY + null -> TextReference.EMPTY + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index 6ebb7a8cc9..f2dc22698e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -19,7 +19,6 @@ internal class SetConfirmationStateAssentTransformer( private val cryptoCurrencyStatusProvider: Provider, private val stakingGasEstimate: StakingGasEstimate, private val pendingActionList: ImmutableList, - private val pendingAction: PendingAction?, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -49,7 +48,6 @@ internal class SetConfirmationStateAssentTransformer( ), validatorState = validatorState.copySealed(isClickable = true), pendingActions = pendingActionList, - pendingActionInProgress = pendingAction, isPrimaryButtonEnabled = true, ) } else { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt index e17aad84b4..817ff94491 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt @@ -1,11 +1,14 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer -internal class SetConfirmationStateInProgressTransformer : Transformer { +internal class SetConfirmationStateInProgressTransformer( + private val pendingAction: PendingAction?, +) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( @@ -19,6 +22,7 @@ internal class SetConfirmationStateInProgressTransformer : Transformer { - val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data - if (initialState?.yieldBalance is InnerYieldBalanceState.Data) { - R.string.staking_stake_more - } else { - R.string.common_next - } - } - StakingStep.Confirmation -> { - val confirmationState = currentState.confirmationState - if (confirmationState is StakingStates.ConfirmationState.Data) { - if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { - R.string.common_close - } else { - when (currentState.routeType) { - RouteType.STAKE -> R.string.common_stake - RouteType.CLAIM -> R.string.common_claim_rewards - RouteType.UNSTAKE -> R.string.common_unstake - RouteType.OTHER -> R.string.common_stake - } - } - } else { - R.string.common_close - } - } - StakingStep.Validators -> R.string.common_continue - StakingStep.Amount, - StakingStep.RewardsValidators, - -> R.string.common_next - } -} - -private fun onPrimaryClick(currentState: StakingUiState) { - when (currentState.currentStep) { - StakingStep.InitialInfo, - StakingStep.Validators, - StakingStep.Amount, - -> currentState.clickIntents.onNextClick() - StakingStep.Confirmation -> { - val confirmationState = currentState.confirmationState - if (confirmationState is StakingStates.ConfirmationState.Data) { - if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { - currentState.clickIntents.onBackClick() - } else { - currentState.clickIntents.onNextClick() - } - } else { - currentState.clickIntents.onBackClick() - } - } - StakingStep.RewardsValidators -> Unit - } -} - -private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) { - StakingStep.InitialInfo, - StakingStep.RewardsValidators, - StakingStep.Confirmation, - -> false - StakingStep.Amount, - StakingStep.Validators, - -> true -} - -private fun isButtonEnabled(uiState: StakingUiState): Pair { - return when (uiState.currentStep) { - StakingStep.InitialInfo -> { - val initialState = uiState.initialInfoState as? StakingStates.InitialInfoState.Data - val isDisplayed = initialState?.isStakeMoreAvailable == true || - initialState?.yieldBalance is InnerYieldBalanceState.Empty - uiState.initialInfoState.isPrimaryButtonEnabled to isDisplayed - } - StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled to true - StakingStep.Confirmation -> uiState.confirmationState.isPrimaryButtonEnabled to true - StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled to false - StakingStep.Validators -> true to true - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index c79fca59ff..af3698a8b8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.tangem.common.ui.amountScreen.AmountScreenContent +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlock import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.res.TangemTheme @@ -45,8 +46,13 @@ internal fun StakingScreen(uiState: StakingUiState) { uiState = uiState, modifier = Modifier.weight(1f), ) - StakingNavigationButtons( - uiState = uiState, + NavigationButtonsBlock( + buttonState = uiState.buttonsState, + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), ) StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig) } @@ -155,7 +161,7 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M amountState = uiState.amountState, state = uiState.confirmationState, clickIntents = uiState.clickIntents, - type = uiState.routeType, + type = uiState.actionType, ) StakingStep.Validators -> { val confirmState = uiState.confirmationState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 0b1ec36847..a6fcb3d67c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -117,7 +117,7 @@ internal class StakingViewModel @Inject constructor( private fun handleOnNextConfirmationClick(pendingAction: PendingAction?) { if (isAssentState()) { viewModelScope.launch { - stateController.update(SetConfirmationStateInProgressTransformer()) + stateController.update(SetConfirmationStateInProgressTransformer(pendingAction)) val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") @@ -128,7 +128,7 @@ internal class StakingViewModel @Inject constructor( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, params = ActionParams( - actionCommonType = getStakingCommonType(), + actionCommonType = value.actionType, integrationId = yield.id, amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided"), @@ -149,7 +149,6 @@ internal class StakingViewModel @Inject constructor( gasEstimate = stakingTransaction.gasEstimate ?: error("No gas estimate available"), txData = TransactionData.Compiled(value = it.hexToBytes()), pendingActionList = confirmationState.pendingActions, - pendingAction = pendingAction, ) } ?: error("No unsigned transaction available") } @@ -170,7 +169,7 @@ internal class StakingViewModel @Inject constructor( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, params = ActionParams( - actionCommonType = getStakingCommonType(), + actionCommonType = value.actionType, integrationId = yield.id, amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided"), @@ -189,7 +188,6 @@ internal class StakingViewModel @Inject constructor( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, stakingGasEstimate = stakingGasEstimate, pendingActionList = pendingActions, - pendingAction = pendingAction, ), ) } @@ -230,17 +228,17 @@ internal class StakingViewModel @Inject constructor( } override fun openRewardsValidators() { - stateController.update { it.copy(routeType = RouteType.CLAIM) } + stateController.update { it.copy(actionType = StakingActionCommonType.PENDING_REWARDS) } onNextClick() } override fun onActiveStake(activeStake: BalanceState) { - val routeType = if (activeStake.pendingActions.isEmpty()) { - RouteType.UNSTAKE + val actionType = if (activeStake.pendingActions.isEmpty()) { + StakingActionCommonType.EXIT } else { - RouteType.OTHER + StakingActionCommonType.PENDING_OTHER } - stateController.update { it.copy(routeType = routeType) } + stateController.update { it.copy(actionType = actionType) } stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, activeStake.cryptoValue)) onNextClick(activeStake.pendingActions) } @@ -256,7 +254,7 @@ internal class StakingViewModel @Inject constructor( } override fun onShareClick() { - // TODO staking analytics event + // TODO add hash to clipboard and send analytics event } fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { @@ -325,7 +323,6 @@ internal class StakingViewModel @Inject constructor( gasEstimate: StakingGasEstimate, txData: TransactionData, pendingActionList: ImmutableList, - pendingAction: PendingAction?, ) { sendTransactionUseCase( txData = txData, @@ -340,7 +337,6 @@ internal class StakingViewModel @Inject constructor( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, stakingGasEstimate = gasEstimate, pendingActionList = pendingActionList, - pendingAction = pendingAction, ), ) // todo add error dialog @@ -398,12 +394,4 @@ internal class StakingViewModel @Inject constructor( (value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState == InnerConfirmationStakingState.ASSENT } - - private fun getStakingCommonType() = when (value.routeType) { - RouteType.STAKE -> StakingActionCommonType.ENTER - RouteType.UNSTAKE -> StakingActionCommonType.EXIT - RouteType.CLAIM, - RouteType.OTHER, - -> StakingActionCommonType.PENDING - } } \ No newline at end of file From e20f93208128c352c0b2168536c412333a7a6983 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Aug 2024 11:24:49 +0300 Subject: [PATCH 15/87] Updated on 2026-08-14 --- build.gradle.kts | 5 + common/ui-charts/build.gradle.kts | 1 + .../tangem/common/ui/charts/MarketChart.kt | 195 +++++++------- .../common/ui/charts/MarketChartMini.kt | 32 +-- .../ui/charts/downsample/LTThreeBuckets.kt | 246 ++++++++++++++++++ .../common/ui/charts/layer/ChartMarker.kt | 112 ++++++++ .../ui/charts/layer/MarketChartLayer.kt | 227 +++++----------- .../common/ui/charts/layer/TimeItemPlacer.kt | 56 ++++ .../common/ui/charts/marker/ChartMarker.kt | 146 ----------- .../common/ui/charts/state/MarketChartData.kt | 6 +- .../charts/state/MarketChartDataProducer.kt | 85 +++--- .../common/ui/charts/state/MarketChartLook.kt | 8 +- .../ui/charts/state/MarketChartRawData.kt | 15 +- .../ui/charts/state/MarketChartState.kt | 75 +++--- .../ui/charts/state/PointValuesConverter.kt | 69 ----- .../state/converter/PointValuesConverter.kt | 23 ++ .../PriceAndTimePointValuesConverter.kt | 108 ++++++++ .../{ => formatter}/AxisLabelFormatter.kt | 4 +- .../state/formatter/FormatterWrapWithCache.kt | 15 ++ .../api/markets/TangemTechMarketsApi.kt | 1 + .../com/tangem/core/ui/res/TangemTheme.kt | 5 + .../core/ui/utils/DateTimeFormatters.kt | 9 +- .../markets/DefaultMarketsTokenRepository.kt | 9 +- .../converters/TokenListConfigConverters.kt | 2 +- .../converters/TokenMarketChartsConverter.kt | 21 +- domain/app-currency/models/build.gradle.kts | 5 + .../domain/appcurrency/model/AppCurrency.kt | 3 + domain/markets/build.gradle.kts | 3 +- .../markets/TokenMarketUpdateRequest.kt | 2 +- .../markets/GetTokenPriceChartUseCase.kt | 13 +- .../repositories/MarketsTokenRepository.kt | 2 +- .../markets/DefaultMarketsEntryComponent.kt | 107 ++++++-- .../markets/MarketsEntryChildFactory.kt | 9 +- .../api/MarketsTokenDetailsComponent.kt | 27 +- .../DefaultMarketsTokenDetailsComponent.kt | 14 +- .../impl/model/MarketsTokenDetailsModel.kt | 210 ++++++++++++++- .../impl/ui/MarketsTokenDetailsContent.kt | 204 ++++++++++++++- .../ui/components/MarketTokenDetailsChart.kt | 69 ++++- .../impl/ui/entity/MarketsTokenDetailsUM.kt | 17 +- .../api/MarketsTokenListComponent.kt | 6 +- .../impl/DefaultMarketsTokenListComponent.kt | 7 +- .../tokenlist/impl/model/MarketsListModel.kt | 4 +- .../converters/MarketsTokenItemConverter.kt | 11 +- .../MarketsListBatchFlowManager.kt | 10 +- .../markets/tokenlist/impl/ui/MarketsList.kt | 98 ++++--- .../ui/components/MarketsListLazyColumn.kt | 2 +- .../MarketChartListItemPreviewDataProvider.kt | 11 +- .../wallet/presentation/WalletFragment.kt | 7 +- .../wallet/state/model/WalletScreenState.kt | 2 + .../presentation/wallet/ui/WalletScreen.kt | 167 ++++++------ .../wallet/viewmodels/WalletViewModel.kt | 2 + gradle/dependencies.toml | 2 +- 52 files changed, 1676 insertions(+), 813 deletions(-) create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/downsample/LTThreeBuckets.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/ChartMarker.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/TimeItemPlacer.kt delete mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt delete mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/PointValuesConverter.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PointValuesConverter.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt rename common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/{ => formatter}/AxisLabelFormatter.kt (76%) create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/formatter/FormatterWrapWithCache.kt diff --git a/build.gradle.kts b/build.gradle.kts index d92b770060..e71d217293 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -94,6 +94,11 @@ val generateComposeMetrics by tasks.registering { "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$outputDirectory", ) + // Compose strong skipping mode + // freeCompilerArgs.addAll( + // "-P", + // "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true", + // ) } } } diff --git a/common/ui-charts/build.gradle.kts b/common/ui-charts/build.gradle.kts index 319c15eebc..58bf27c3a5 100644 --- a/common/ui-charts/build.gradle.kts +++ b/common/ui-charts/build.gradle.kts @@ -22,4 +22,5 @@ dependencies { implementation(deps.compose.material3) implementation(deps.compose.ui.tooling) implementation(deps.compose.ui.utils) + implementation(deps.kotlin.immutable.collections) } \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt index 58c95c1451..3dca540806 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -3,7 +3,6 @@ package com.tangem.common.ui.charts import android.content.res.Configuration import androidx.annotation.FloatRange import androidx.compose.foundation.background -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -12,10 +11,11 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFontFamilyResolver -import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontSynthesis import androidx.compose.ui.text.font.FontWeight @@ -23,26 +23,32 @@ import androidx.compose.ui.text.font.resolveAsTypeface import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import com.patrykandpatrick.vico.compose.cartesian.* +import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis import com.patrykandpatrick.vico.compose.cartesian.axis.rememberCustomStartAxis +import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart +import com.patrykandpatrick.vico.compose.cartesian.rememberVicoScrollState +import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState import com.patrykandpatrick.vico.compose.common.of import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout import com.patrykandpatrick.vico.core.cartesian.Zoom -import com.patrykandpatrick.vico.core.cartesian.axis.* +import com.patrykandpatrick.vico.core.cartesian.axis.AxisPosition +import com.patrykandpatrick.vico.core.cartesian.axis.BaseAxis +import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis +import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter -import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerTarget import com.patrykandpatrick.vico.core.common.Dimensions import com.patrykandpatrick.vico.core.common.component.LineComponent import com.patrykandpatrick.vico.core.common.shape.Shape +import com.tangem.common.ui.charts.layer.TimeItemPlacer import com.tangem.common.ui.charts.layer.rememberMarketChartLayer -import com.tangem.common.ui.charts.marker.rememberTangemChartMarker +import com.tangem.common.ui.charts.layer.rememberTangemChartMarker import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider import com.tangem.common.ui.charts.state.* import com.tangem.core.ui.components.SpacerH16 @@ -50,6 +56,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import java.math.BigDecimal import java.math.RoundingMode @@ -73,70 +80,72 @@ fun MarketChart( splitChartSegmentColor: Color = TangemTheme.colors.icon.inactive, @FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float = 0.24f, @FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float = 0.24f, - noChartContent: @Composable BoxScope.() -> Unit, ) { var canvasWidth by remember { mutableIntStateOf(0) } - var canvasHeight by remember { mutableIntStateOf(0) } + var chartHeight by remember { mutableIntStateOf(0) } - val layer = rememberLayerFromState( - state = state, - splitChartSegmentColor = splitChartSegmentColor, - backgroundColorAlpha = backgroundColorAlpha, - backgroundSplitChartSegmentColorAlpha = backgroundSplitChartSegmentColorAlpha, - canvasHeight = canvasHeight, + val layer = rememberMarketChartLayer( + lineColor = state.chartColor, + backgroundLineColor = state.chartColor.copy(alpha = backgroundColorAlpha), + secondLineColor = splitChartSegmentColor, + backgroundSecondLineColor = splitChartSegmentColor.copy(alpha = backgroundSplitChartSegmentColorAlpha), + secondColorOnTheRightSide = state.markerHighlightRightSide.not(), + markerFraction = state.markerFraction, + axisValueOverrider = AxisValueOverrider.fixed(), + canvasHeight = chartHeight, ) + + val marker = rememberTangemChartMarker(color = state.chartColor) + val chart = rememberCartesianChart( layer, - startAxis = rememberMarketChartStartAxis( - yValueFormatter = state.yValueFormatter, - ), - bottomAxis = rememberMarketChartBottomAxis( - xValueFormatter = state.xValueFormatter, - ), + startAxis = rememberMarketChartStartAxis(state.yValueFormatter), + bottomAxis = rememberMarketChartBottomAxis(state.xValueFormatter), + horizontalLayout = HorizontalLayout.FullWidth(), + markerVisibilityListener = rememberMarketVisibilityListener(canvasWidth, state), + marker = marker, ) - val marker = rememberTangemChartMarker( - color = state.chartColor, - innerCircleColor = Color.White, - ) - val density = LocalDensity.current + + // we need to calculate what the overall height should be in order to get the correct height of the graph + val bottomAxisHeight = with(LocalDensity.current) { + TangemTheme.typography.caption2.fontSize.toPx().toInt() + TangemTheme.dimens.spacing26.toPx().toInt() + } CartesianChartHost( - modifier = modifier.onGloballyPositioned { - with(density) { + modifier = modifier + .onGloballyPositioned { canvasWidth = it.size.width - canvasHeight = if (it.size.height != 0) { - // FIXME get height bounded to min max chart points - it.size.height - 20.dp.toPx().toInt() - 27.dp.toPx().toInt() + chartHeight = if (it.size.height != 0) { + it.size.height - bottomAxisHeight } else { 0 } - } - }, + }, chart = chart, modelProducer = state.modelProducer, scrollState = rememberVicoScrollState(scrollEnabled = false), zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), - horizontalLayout = HorizontalLayout.fullWidth(), - markerVisibilityListener = state.rememberMarketVisibilityListener(canvasWidth = canvasWidth), - diffAnimationSpec = null, - marker = marker, - placeholder = noChartContent, + animationSpec = null, ) } @Composable -private fun MarketChartState.rememberMarketVisibilityListener(canvasWidth: Int): CartesianMarkerVisibilityListener { - val state = this - return remember(state.markerVisibilityListener, canvasWidth) { +private fun rememberMarketVisibilityListener( + canvasWidth: Int, + state: MarketChartState, +): CartesianMarkerVisibilityListener { + val haptic = LocalHapticFeedback.current + return remember(state, canvasWidth) { val maxCanvasXFloat = canvasWidth.toFloat().takeIf { it != 0f } object : CartesianMarkerVisibilityListener { override fun onShown(marker: CartesianMarker, targets: List) { - state.stopDrawingAnimation() val xCanvas = (targets[0] as LineCartesianLayerMarkerTarget).canvasX state.markerFraction = maxCanvasXFloat?.let { xCanvas / it } state.markerVisibilityListener.onShown(marker, targets) + + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) } override fun onHidden(marker: CartesianMarker) { @@ -149,38 +158,29 @@ private fun MarketChartState.rememberMarketVisibilityListener(canvasWidth: Int): state.markerFraction = maxCanvasXFloat?.let { xCanvas / it } state.markerVisibilityListener.onUpdated(marker, targets) + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) } } } } -@Composable -private fun rememberLayerFromState( - state: MarketChartState, - splitChartSegmentColor: Color, - @FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float, - @FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float, - canvasHeight: Int, -): LineCartesianLayer { - return rememberMarketChartLayer( - lineColor = state.chartColor, - backgroundLineColor = state.chartColor.copy(alpha = backgroundColorAlpha), - secondLineColor = splitChartSegmentColor, - backgroundSecondLineColor = splitChartSegmentColor.copy(alpha = backgroundSplitChartSegmentColorAlpha), - secondColorOnTheRightSide = state.markerHighlightRightSide.not(), - startDrawingAnimation = state.startDrawingAnimationState, - markerFraction = state.markerFraction, - axisValueOverrider = AxisValueOverrider.adaptiveYValues(yFraction = 1.2f, round = true), // FIXME ? - canvasHeight = canvasHeight, - ) -} - @Composable private fun rememberMarketChartStartAxis( yValueFormatter: CartesianValueFormatter, ): VerticalAxis { + val textStyle = TangemTheme.typography.caption2 + val resolver = LocalFontFamilyResolver.current + val typeface by remember(resolver, textStyle) { + resolver.resolveAsTypeface( + fontFamily = textStyle.fontFamily, + fontWeight = textStyle.fontWeight ?: FontWeight.Normal, + fontStyle = textStyle.fontStyle ?: FontStyle.Normal, + fontSynthesis = textStyle.fontSynthesis ?: FontSynthesis.All, + ) + } + return rememberCustomStartAxis( - axis = null, + line = null, tick = null, guideline = null, labelGuideline = rememberChartAxisGuidelineComponent( @@ -194,38 +194,44 @@ private fun rememberMarketChartStartAxis( end = TangemTheme.dimens.spacing4, ), textSize = TangemTheme.typography.caption2.fontSize, - typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(), + typeface = typeface, ), horizontalLabelPosition = VerticalAxis.HorizontalLabelPosition.Inside, verticalLabelPosition = VerticalAxis.VerticalLabelPosition.Center, - itemPlacer = AxisItemPlacer.Vertical.count({ GUIDELINES_COUNT }, false), + itemPlacer = VerticalAxis.ItemPlacer.count({ GUIDELINES_COUNT }, false), valueFormatter = yValueFormatter, ) } @Composable -fun rememberMarketChartBottomAxis( +private fun rememberMarketChartBottomAxis( xValueFormatter: CartesianValueFormatter, ): HorizontalAxis { + val textStyle = TangemTheme.typography.caption2 + + val resolver = LocalFontFamilyResolver.current + + val typeface by remember(resolver, textStyle) { + resolver.resolveAsTypeface( + fontFamily = textStyle.fontFamily, + fontWeight = textStyle.fontWeight ?: FontWeight.Normal, + fontStyle = textStyle.fontStyle ?: FontStyle.Normal, + fontSynthesis = textStyle.fontSynthesis ?: FontSynthesis.All, + ) + } + return rememberBottomAxis( label = rememberAxisLabelComponent( color = TangemTheme.colors.text.tertiary, textSize = TangemTheme.typography.caption2.fontSize, - padding = Dimensions.of(top = TangemTheme.dimens.spacing20), - typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(), + padding = Dimensions.of(top = TangemTheme.dimens.spacing26), + typeface = typeface, ), tick = null, - axis = null, + line = null, guideline = null, - sizeConstraint = BaseAxis.SizeConstraint.Exact(sizeDp = 37f), // FIXME ? - itemPlacer = remember { - AxisItemPlacer.Horizontal.default( - spacing = 25, // FIXME ? - offset = 60, // FIXME ? - shiftExtremeTicks = false, - addExtremeLabelPadding = false, - ) - }, + sizeConstraint = BaseAxis.SizeConstraint.Auto(), + itemPlacer = remember { TimeItemPlacer() }, valueFormatter = xValueFormatter, ) } @@ -245,19 +251,6 @@ private fun rememberChartAxisGuidelineComponent(color: Color): LineComponent { ) } -@Composable -internal fun TextStyle.toGraphicsTypeFace(): android.graphics.Typeface { - val resolver = LocalFontFamilyResolver.current - return remember(resolver, this) { - resolver.resolveAsTypeface( - fontFamily = this.fontFamily, - fontWeight = this.fontWeight ?: FontWeight.Normal, - fontStyle = this.fontStyle ?: FontStyle.Normal, - fontSynthesis = this.fontSynthesis ?: FontSynthesis.All, - ) - }.value -} - // region Preview @Suppress("LongMethod") @@ -275,7 +268,6 @@ private fun MarketChartPreview( chartLook = MarketChartLook( type = MarketChartLook.Type.Growing, markerHighlightRightSide = true, - animationOnDataChange = true, ) } } @@ -283,8 +275,8 @@ private fun MarketChartPreview( LaunchedEffect(key1 = Unit) { dataProducer.runTransactionSuspend { chartData = MarketChartData.Data( - x = x, - y = y, + x = x.toImmutableList(), + y = y.toImmutableList(), ) updateLook { it.copy( @@ -338,13 +330,9 @@ private fun MarketChartPreview( splitChartSegmentColor = TangemTheme.colors.icon.inactive, backgroundSplitChartSegmentColorAlpha = 0.24f, backgroundColorAlpha = 0.24f, - noChartContent = { }, ) SpacerH16() - Button(onClick = { chartState.startDrawingAnimation() }) { - Text("Start drawing animation") - } Button( onClick = { dataProducer.runTransaction { @@ -365,7 +353,7 @@ private fun MarketChartPreview( updateData { MarketChartData.Data( x = it.x, - y = it.y.reversed(), + y = it.y.reversed().toImmutableList(), ) } } @@ -374,15 +362,6 @@ private fun MarketChartPreview( ) { Text("Change Data") } - Button( - onClick = { - dataProducer.runTransaction { - updateLook { it.copy(animationOnDataChange = it.animationOnDataChange.not()) } - } - }, - ) { - Text("Change animationOnDataChange = ${look.animationOnDataChange}") - } Button( onClick = { diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt index d9902450d3..2a124ed8d2 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt @@ -12,19 +12,21 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.patrykandpatrick.vico.compose.cartesian.* +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLine import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer -import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec -import com.patrykandpatrick.vico.compose.common.shader.BrushShader +import com.patrykandpatrick.vico.compose.common.shader.toDynamicShader import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout import com.patrykandpatrick.vico.core.cartesian.Zoom import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel +import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer import com.patrykandpatrick.vico.core.common.shader.ColorShader import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.toImmutableList import kotlin.random.Random @Composable @@ -44,18 +46,19 @@ fun MarketChartMini( MarketChartLook.Type.Falling -> fallingColor } - val lineSpec = rememberLineSpec( + val lineSpec = rememberLine( shader = ColorShader(lineColor.toArgb()), thickness = 1.dp, - backgroundShader = BrushShader( - brush = Brush.verticalGradient( - colors = listOf(lineColor.copy(alpha = 0.22f), Color.Transparent), - ), - ), + backgroundShader = Brush.verticalGradient( + colors = listOf(lineColor.copy(alpha = 0.22f), Color.Transparent), + ).toDynamicShader(), ) - val layer = rememberLineCartesianLayer(listOf(lineSpec)) - val chart = rememberCartesianChart(layer) + val layer = rememberLineCartesianLayer(LineCartesianLayer.LineProvider.series(lineSpec)) + val chart = rememberCartesianChart( + layer, + horizontalLayout = HorizontalLayout.fullWidth(), + ) CartesianChartHost( modifier = modifier, @@ -63,7 +66,6 @@ fun MarketChartMini( model = model, zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), scrollState = rememberVicoScrollState(scrollEnabled = false), - horizontalLayout = HorizontalLayout.fullWidth(), ) } @@ -74,8 +76,8 @@ fun MarketChartMini( @Composable private fun Preview() { val data = MarketChartRawData( - x = List(20) { Random.nextFloat() }, - y = List(20) { Random.nextFloat() }, + x = List(20) { Random.nextFloat().toDouble() }.toImmutableList(), + y = List(20) { Random.nextFloat().toDouble() }.toImmutableList(), ) TangemThemePreview { @@ -92,8 +94,8 @@ private fun Preview() { @Composable private fun PreviewColumn() { val data = MarketChartRawData( - x = List(20) { Random.nextFloat() }, - y = List(20) { Random.nextFloat() }, + x = List(20) { Random.nextFloat().toDouble() }.toImmutableList(), + y = List(20) { Random.nextFloat().toDouble() }.toImmutableList(), ) TangemThemePreview { diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/downsample/LTThreeBuckets.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/downsample/LTThreeBuckets.kt new file mode 100644 index 0000000000..59bf621547 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/downsample/LTThreeBuckets.kt @@ -0,0 +1,246 @@ +package com.tangem.common.ui.charts.downsample + +import kotlin.math.max + +/** + * ========================================================= + + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= + * + * Downsamples the given data points to the desired number of buckets (points + 2). + * +[REDACTED_AUTHOR] + */ +object LTThreeBuckets { + + fun downsample(x: List, y: List, desiredBuckets: Int): Result { + require(x.size == y.size) { "X and Y must have the same size" } + require(desiredBuckets > 0) { "Desired buckets must be greater than 0" } + + val points = x.zip(y).mapIndexed { index, (x, y) -> Point(index, x, y) } + val results = mutableListOf() + + points.onPassBucketize(desiredBuckets) + .sliding(size = 3, step = 1) + .map { buckets -> Triangle.of(buckets) } + .fastForEach { triangle -> + if (results.isEmpty()) { + results.add(triangle.getFirst()) + } + + results.add(triangle.getResult()) + + if (results.size == desiredBuckets + 1) { + results.add(triangle.getLast()) + } + } + + val xRes = ArrayList(points.size) + val yRes = ArrayList(points.size) + val indexesRes = ArrayList(points.size) + + results.fastForEach { + xRes.add(it.x) + yRes.add(it.y) + indexesRes.add(it.originalIndex!!) + } + + return Result( + originalIndexes = indexesRes, + x = xRes, + y = yRes, + ) + } + + data class Result( + val originalIndexes: List, + val x: List, + val y: List, + ) +} + +private fun List.onPassBucketize(desiredBucketsCount: Int): List { + val middleSize = size - 2 + val bucketSize = middleSize / desiredBucketsCount + val remainingElements = middleSize % desiredBucketsCount + + require(bucketSize != 0) { + "Can't produce $desiredBucketsCount buckets from an input series of ${middleSize + 2} elements" + } + + val buckets = mutableListOf() + + // Add first point as the only point in the first bucket + buckets.add(Bucket.of(this[0])) + + var rest = this.subList(1, this.lastIndex) + + // Add middle buckets. + // When inputSize is not a multiple of desiredBuckets, + // remaining elements are equally distributed on the first buckets. + while (buckets.size < desiredBucketsCount + 1) { + val size = if (buckets.size <= remainingElements) bucketSize + 1 else bucketSize + buckets.add(Bucket.of(rest.subList(0, size))) + rest = rest.subList(size, rest.size) + } + + // Add last point as the only point in the last bucket + buckets.add(Bucket.of(this.last())) + + return buckets +} + +private fun List.sliding(size: Int, step: Int): List> { + val window = max(size, step) + val buffer = ArrayDeque() + var totalIn = 0 + + val lists = mutableListOf>() + + fastForEach { p -> + buffer.add(p) + ++totalIn + if (buffer.size == window) { + val batch = buffer.take(size) + lists.add(batch) + + repeat(step) { + buffer.removeFirst() + } + } + } + + if (buffer.isNotEmpty()) { + val totalOut = max(0, (totalIn + step - size - 1) / step) + 1 + if (totalOut > lists.size) { + val batch = buffer.take(size) + lists.add(batch) + } + } + + return lists +} + +private data class Point( + val originalIndex: Int? = null, + val x: Double, + val y: Double, +) + +private data class Bucket( + val data: List, + val center: Point, + val result: Point, + val first: Point, + val last: Point, +) { + companion object { + private fun centerBetweenPoints(a: Point, b: Point): Point { + val vector = Point( + x = b.x - a.x, + y = b.y - a.y, + ) + val halfVector = Point( + x = vector.x / 2, + y = vector.y / 2, + ) + + return Point( + x = a.x + halfVector.x, + y = a.y + halfVector.y, + ) + } + + fun of(points: List): Bucket { + val first = points.first() + val last = points.last() + + return Bucket( + data = points, + center = centerBetweenPoints(first, last), + result = first, + first = first, + last = last, + ) + } + + fun of(point: Point): Bucket { + return Bucket( + data = listOf(point), + center = point, + result = point, + first = point, + last = point, + ) + } + } +} + +private data class Triangle( + val left: Bucket, + val center: Bucket, + val right: Bucket, +) { + fun getResult(): Point { + return center.data.map { Area.ofTriangle(left.result, it, right.center) } + .maxByOrNull { it.value } + ?.generator + ?: error("Can't obtain max area triangle") + } + + fun getFirst(): Point { + return left.first + } + + fun getLast(): Point { + return right.last + } + + companion object { + fun of(buckets: List): Triangle { + return Triangle( + left = buckets[0], + center = buckets[1], + right = buckets[2], + ) + } + } +} + +private data class Area( + val generator: Point, + val value: Double, +) { + companion object { + fun ofTriangle(a: Point, b: Point, c: Point): Area { + val addends = listOf( + a.x * (b.y - c.y), + b.x * (c.y - a.y), + c.x * (a.y - b.y), + ) + val sum = addends.sum() + val value = kotlin.math.abs(sum / 2) + + return Area(b, value) + } + } +} + +inline fun List.fastForEach(action: (T) -> Unit) { + for (index in indices) { + val item = get(index) + action(item) + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/ChartMarker.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/ChartMarker.kt new file mode 100644 index 0000000000..0005e47d15 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/ChartMarker.kt @@ -0,0 +1,112 @@ +package com.tangem.common.ui.charts.layer + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.patrykandpatrick.vico.compose.common.component.rememberUnboundedLineComponent +import com.patrykandpatrick.vico.compose.common.component.shapeComponent +import com.patrykandpatrick.vico.compose.common.of +import com.patrykandpatrick.vico.compose.common.shape.dashed +import com.patrykandpatrick.vico.core.cartesian.* +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerValueFormatter +import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker +import com.patrykandpatrick.vico.core.common.Dimensions +import com.patrykandpatrick.vico.core.common.LayeredComponent +import com.patrykandpatrick.vico.core.common.component.Component +import com.patrykandpatrick.vico.core.common.component.TextComponent +import com.patrykandpatrick.vico.core.common.shape.Shape +import com.tangem.core.ui.res.TangemTheme + +/** + * @param color The color of the indicator and guideline. + * @param innerCircleColor The color of the inner circle of the indicator. + * + * @return A [CartesianMarker] that consists of a dashed guideline and a layered indicator with a shadow effect. + */ +@Composable +internal fun rememberTangemChartMarker(color: Color): CartesianMarker { + val guideline = rememberUnboundedLineComponent( + color = color, + verticalAddDrawSpace = TangemTheme.dimens.spacing24, + shape = remember { Shape.dashed(Shape.Rectangle, 4.dp, 4.dp) }, + ) + + return remember(guideline) { + val outColor = guideline.color + + object : DefaultCartesianMarker( + label = TextComponent(textSizeSp = 0f), + indicator = ::indicator, + indicatorSizeDp = INDICATOR_SIZE_DP, + guideline = guideline, + valueFormatter = object : CartesianMarkerValueFormatter { + override fun format( + context: CartesianDrawContext, + targets: List, + ): CharSequence = "" + }, + ) { + override fun updateInsets( + context: CartesianMeasureContext, + horizontalDimensions: HorizontalDimensions, + model: CartesianChartModel, + insets: Insets, + ) { + with(context) { + super.updateInsets(context, horizontalDimensions, model, insets) + val baseShadowInsetDp = + CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER * LABEL_BACKGROUND_SHADOW_RADIUS_DP + val topInset = (baseShadowInsetDp - LABEL_BACKGROUND_SHADOW_DY_DP).pixels + val bottomInset = (baseShadowInsetDp + LABEL_BACKGROUND_SHADOW_DY_DP).pixels + insets.ensureValuesAtLeast(top = topInset, bottom = bottomInset) + } + } + + override fun CartesianDrawContext.drawIndicator(x: Float, y: Float, color: Int, halfIndicatorSize: Float) { + val indicator = indicator ?: return + cacheStore + .getOrSet(keyNamespace, indicator, outColor) { indicator.invoke(outColor) } + .draw( + this, + x - halfIndicatorSize, + y - halfIndicatorSize, + x + halfIndicatorSize, + y + halfIndicatorSize, + ) + } + } + } +} + +private fun indicator(color: Int): Component { + val composeColor = Color(color) + + return LayeredComponent( + rear = shapeComponent( + color = composeColor.copy(alpha = INDICATOR_REAR_COLOR_ALPHA), + shape = Shape.Pill, + ), + front = LayeredComponent( + rear = shapeComponent( + color = composeColor, + shape = Shape.Pill, + ), + front = shapeComponent( + color = Color.White, + shape = Shape.Pill, + ), + padding = indicatorPadding, + ), + padding = indicatorPadding, + ) +} + +private val indicatorPadding = Dimensions.of(3.dp) +private const val LABEL_BACKGROUND_SHADOW_RADIUS_DP = 4f +private const val LABEL_BACKGROUND_SHADOW_DY_DP = 2f +private const val CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER = 1.4f +private const val INDICATOR_SIZE_DP = 16f +private const val INDICATOR_REAR_COLOR_ALPHA = .24f \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt index b224a1838d..a01ceb3d22 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt @@ -2,9 +2,6 @@ package com.tangem.common.ui.charts.layer import android.content.res.Configuration import androidx.annotation.FloatRange -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animate -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -20,22 +17,22 @@ import androidx.compose.ui.unit.dp import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost import com.patrykandpatrick.vico.compose.cartesian.fullWidth import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer -import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec -import com.patrykandpatrick.vico.compose.cartesian.layer.rememberSplitLineSpec +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberSplitLine import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState -import com.patrykandpatrick.vico.compose.common.shader.BrushShader +import com.patrykandpatrick.vico.compose.common.shader.toDynamicShader import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout import com.patrykandpatrick.vico.core.cartesian.Zoom import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer -import com.patrykandpatrick.vico.core.common.shader.ColorShader import com.patrykandpatrick.vico.core.common.shader.DynamicShader import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal /** @@ -59,117 +56,50 @@ internal fun rememberMarketChartLayer( backgroundLineColor: Color, secondLineColor: Color, backgroundSecondLineColor: Color, - startDrawingAnimation: MutableState, axisValueOverrider: AxisValueOverrider, secondColorOnTheRightSide: Boolean, @FloatRange(from = 0.0, to = 1.0) markerFraction: Float?, canvasHeight: Int, ): LineCartesianLayer { - var animationFraction: Float? by remember { mutableStateOf(null) } + val backgroundColorLineGradient = persistentListOf(backgroundLineColor, Color.Transparent) + val backgroundSecondLineColorGradient = persistentListOf(backgroundSecondLineColor, Color.Transparent) - LaunchedEffect(startDrawingAnimation.value) { - animationFraction = null - if (startDrawingAnimation.value) { - animate( - initialValue = 0f, - targetValue = 1f, - animationSpec = tween(easing = LinearEasing, durationMillis = 1000), - ) { start, _ -> - if (start == 1f) { - animationFraction = null - startDrawingAnimation.value = false - } else { - animationFraction = start - } - } - } - } + val markerSet = markerFraction != null - return rememberRawMarketChartLayer( - lineColor = lineColor, - backgroundLineColor = backgroundLineColor, - secondLineColor = secondLineColor, - backgroundSecondLineColor = backgroundSecondLineColor, + return rememberLayer( + fractionValue = markerFraction ?: 0f, axisValueOverrider = axisValueOverrider, - secondColorOnTheRightSide = secondColorOnTheRightSide, - markerFraction = markerFraction, - animationFraction = animationFraction, canvasHeight = canvasHeight, + lineColor = if (markerFraction != null) { + secondLineColor + } else { + lineColor + }, + backLineColor = if (markerSet && !secondColorOnTheRightSide) { + backgroundSecondLineColorGradient + } else { + backgroundColorLineGradient + }, + lineColorRight = when { + markerSet && secondColorOnTheRightSide -> secondLineColor + else -> lineColor + }, + backLineColorRight = when { + markerSet && secondColorOnTheRightSide -> backgroundSecondLineColorGradient + else -> backgroundColorLineGradient + }, ) } @Suppress("LongParameterList") -@Composable -private fun rememberRawMarketChartLayer( - lineColor: Color, - backgroundLineColor: Color, - secondLineColor: Color, - backgroundSecondLineColor: Color, - axisValueOverrider: AxisValueOverrider, - canvasHeight: Int, - secondColorOnTheRightSide: Boolean = false, - @FloatRange(from = 0.0, to = 1.0) markerFraction: Float? = null, - @FloatRange(from = 0.0, to = 1.0) animationFraction: Float? = null, -): LineCartesianLayer { - val backgroundColorLineGradient = listOf(backgroundLineColor, Color.Transparent) - val backgroundSecondLineColorGradient = listOf(backgroundSecondLineColor, Color.Transparent) - - val markerSet = markerFraction != null - val animationRunning = animationFraction != null && animationFraction != 1f - - val layerColors = when { - !animationRunning && markerSet && secondColorOnTheRightSide -> { - LayerColors( - lineColor = lineColor, - backLineColor = backgroundColorLineGradient, - lineColorRight = secondLineColor, - backLineColorRight = backgroundSecondLineColorGradient, - ) - } - !animationRunning && markerSet && !secondColorOnTheRightSide -> { - LayerColors( - lineColor = secondLineColor, - backLineColor = backgroundSecondLineColorGradient, - lineColorRight = lineColor, - backLineColorRight = backgroundColorLineGradient, - ) - } - animationRunning -> { - LayerColors( - lineColor = lineColor, - backLineColor = backgroundColorLineGradient, - lineColorRight = Color.Transparent, - backLineColorRight = listOf(Color.Transparent, Color.Transparent), - ) - } - else -> { - LayerColors( - lineColor = lineColor, - backLineColor = backgroundColorLineGradient, - ) - } - } - - return rememberLayer( - fractionValue = animationFraction ?: markerFraction, - axisValueOverrider = axisValueOverrider, - layerColors = layerColors, - canvasHeight = canvasHeight, - ) -} - -private data class LayerColors( - val lineColor: Color, - val backLineColor: List, - val lineColorRight: Color? = null, - val backLineColorRight: List? = null, -) - @Composable private fun rememberLayer( - fractionValue: Float?, + fractionValue: Float, axisValueOverrider: AxisValueOverrider, - layerColors: LayerColors, + lineColor: Color, + backLineColor: ImmutableList, + lineColorRight: Color, + backLineColorRight: ImmutableList, canvasHeight: Int, ): LineCartesianLayer { val endGradientColorPosition = if (canvasHeight != 0) { @@ -178,47 +108,27 @@ private fun rememberLayer( Float.POSITIVE_INFINITY } + val alineColor = remember(lineColor) { lineColor.toArgb() } + val alineColorRight = remember(lineColorRight) { lineColorRight.toArgb() } + return rememberLineCartesianLayer( - listOf( - if (layerColors.lineColorRight == null || layerColors.backLineColorRight == null || fractionValue == null) { - rememberLineSpec( - shader = remember(layerColors.lineColor) { ColorShader(color = layerColors.lineColor.toArgb()) }, - backgroundShader = remember(layerColors.backLineColor, endGradientColorPosition) { - BrushShader( - brush = Brush.verticalGradient( - colors = layerColors.backLineColor, - endY = endGradientColorPosition, - ), - ) - }, - ) - } else { - rememberSplitLineSpec( - shader = remember(layerColors.lineColor, layerColors.lineColorRight, fractionValue) { - DynamicShader.Companion.horizontalGradient( - colors = intArrayOf(layerColors.lineColor.toArgb(), layerColors.lineColorRight.toArgb()), - positions = floatArrayOf(fractionValue, fractionValue), - ) - }, - backgroundShaderFirst = remember(layerColors.backLineColor, endGradientColorPosition) { - BrushShader( - brush = Brush.verticalGradient( - colors = layerColors.backLineColor, - endY = endGradientColorPosition, - ), - ) - }, - backgroundShaderSecond = remember(layerColors.backLineColorRight, endGradientColorPosition) { - BrushShader( - brush = Brush.verticalGradient( - colors = layerColors.backLineColorRight, - endY = endGradientColorPosition, - ), - ) - }, - xSplitFraction = fractionValue, - ) - }, + LineCartesianLayer.LineProvider.series( + rememberSplitLine( + shader = DynamicShader.Companion.horizontalGradient( + colors = intArrayOf(alineColor, alineColorRight), + positions = floatArrayOf(fractionValue, fractionValue), + ), + backgroundShaderFirst = Brush.verticalGradient( + colors = backLineColor, + endY = endGradientColorPosition, + ).toDynamicShader(), + backgroundShaderSecond = Brush.verticalGradient( + colors = backLineColorRight, + endY = endGradientColorPosition, + ).toDynamicShader(), + xSplitFraction = fractionValue, + thickness = 1.dp, + ), ), axisValueOverrider = axisValueOverrider, ) @@ -250,25 +160,26 @@ private fun LayerChartPreview( CartesianChartHost( modifier = Modifier.fillMaxWidth(), chart = rememberCartesianChart( - rememberRawMarketChartLayer( + rememberMarketChartLayer( lineColor = lineColor, backgroundLineColor = lineColor.copy(alpha = 0.24f), secondLineColor = Color.Gray, backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f), secondColorOnTheRightSide = true, axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + markerFraction = 0.35f, canvasHeight = 495, ), + horizontalLayout = HorizontalLayout.fullWidth(), ), zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), - horizontalLayout = HorizontalLayout.fullWidth(), model = model, ) CartesianChartHost( modifier = Modifier.fillMaxWidth(), chart = rememberCartesianChart( - rememberRawMarketChartLayer( + rememberMarketChartLayer( lineColor = lineColor, backgroundLineColor = lineColor.copy(alpha = 0.24f), secondLineColor = Color.Gray, @@ -278,16 +189,16 @@ private fun LayerChartPreview( axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), canvasHeight = 495, ), + horizontalLayout = HorizontalLayout.fullWidth(), ), zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), - horizontalLayout = HorizontalLayout.fullWidth(), model = model, ) CartesianChartHost( modifier = Modifier.fillMaxWidth(), chart = rememberCartesianChart( - rememberRawMarketChartLayer( + rememberMarketChartLayer( lineColor = lineColor, backgroundLineColor = lineColor.copy(alpha = 0.24f), secondLineColor = Color.Gray, @@ -297,29 +208,9 @@ private fun LayerChartPreview( axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), canvasHeight = 495, ), + horizontalLayout = HorizontalLayout.fullWidth(), ), zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), - horizontalLayout = HorizontalLayout.fullWidth(), - model = model, - ) - - CartesianChartHost( - modifier = Modifier.fillMaxWidth(), - chart = rememberCartesianChart( - rememberRawMarketChartLayer( - lineColor = lineColor, - backgroundLineColor = lineColor.copy(alpha = 0.24f), - secondLineColor = lineColor, - backgroundSecondLineColor = lineColor.copy(alpha = 0.24f), - markerFraction = 0.35f, - secondColorOnTheRightSide = true, - animationFraction = 0.7f, - axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), - canvasHeight = 495, - ), - ), - zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), - horizontalLayout = HorizontalLayout.fullWidth(), model = model, ) } diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/TimeItemPlacer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/TimeItemPlacer.kt new file mode 100644 index 0000000000..034af69926 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/TimeItemPlacer.kt @@ -0,0 +1,56 @@ +package com.tangem.common.ui.charts.layer + +import com.patrykandpatrick.vico.core.cartesian.CartesianDrawContext +import com.patrykandpatrick.vico.core.cartesian.CartesianMeasureContext +import com.patrykandpatrick.vico.core.cartesian.HorizontalDimensions +import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis +import com.patrykandpatrick.vico.core.cartesian.data.ChartValues + +@Suppress("MagicNumber") +class TimeItemPlacer : HorizontalAxis.ItemPlacer { + + private val ChartValues.measuredLabelValues + get() = buildList { + // produce exactly 6 values distributed evenly + val xLength = maxX - minX + val xStep = xLength / 7 + + repeat(times = 6) { + add(minX + xStep * (it + 1)) + } + } + + override fun getEndHorizontalAxisInset( + context: CartesianMeasureContext, + horizontalDimensions: HorizontalDimensions, + tickThickness: Float, + maxLabelWidth: Float, + ): Float = 0f + + override fun getStartHorizontalAxisInset( + context: CartesianMeasureContext, + horizontalDimensions: HorizontalDimensions, + tickThickness: Float, + maxLabelWidth: Float, + ): Float = 0f + + override fun getHeightMeasurementLabelValues( + context: CartesianMeasureContext, + horizontalDimensions: HorizontalDimensions, + fullXRange: ClosedFloatingPointRange, + maxLabelWidth: Float, + ): List = context.chartValues.measuredLabelValues + + override fun getLabelValues( + context: CartesianDrawContext, + visibleXRange: ClosedFloatingPointRange, + fullXRange: ClosedFloatingPointRange, + maxLabelWidth: Float, + ): List = context.chartValues.measuredLabelValues + + override fun getWidthMeasurementLabelValues( + context: CartesianMeasureContext, + horizontalDimensions: HorizontalDimensions, + fullXRange: ClosedFloatingPointRange, + ): List = context.chartValues.measuredLabelValues +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt deleted file mode 100644 index e27ce63a70..0000000000 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt +++ /dev/null @@ -1,146 +0,0 @@ -package com.tangem.common.ui.charts.marker - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost -import com.patrykandpatrick.vico.compose.cartesian.fullWidth -import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer -import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec -import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart -import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState -import com.patrykandpatrick.vico.compose.common.component.rememberLayeredComponent -import com.patrykandpatrick.vico.compose.common.component.rememberShapeComponent -import com.patrykandpatrick.vico.compose.common.component.rememberUnboundedLineComponent -import com.patrykandpatrick.vico.compose.common.of -import com.patrykandpatrick.vico.compose.common.shader.color -import com.patrykandpatrick.vico.compose.common.shape.dashed -import com.patrykandpatrick.vico.core.cartesian.* -import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider -import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel -import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel -import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker -import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker -import com.patrykandpatrick.vico.core.common.Dimensions -import com.patrykandpatrick.vico.core.common.component.TextComponent -import com.patrykandpatrick.vico.core.common.shader.DynamicShader -import com.patrykandpatrick.vico.core.common.shape.Shape -import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import java.math.BigDecimal - -/** - * @param color The color of the indicator and guideline. - * @param innerCircleColor The color of the inner circle of the indicator. - * - * @return A [CartesianMarker] that consists of a dashed guideline and a layered indicator with a shadow effect. - */ -@Composable -internal fun rememberTangemChartMarker(color: Color, innerCircleColor: Color): CartesianMarker { - val indicatorFrontComponent = rememberShapeComponent( - shape = Shape.Pill, - color = innerCircleColor, - ) - val indicatorCenterComponent = rememberShapeComponent( - shape = Shape.Pill, - color = color, - ) - val indicatorRearComponent = rememberShapeComponent( - shape = Shape.Pill, - color = if (color == Color.Transparent) { - Color.Transparent - } else { - color.copy(alpha = INDICATOR_REAR_COLOR_ALPHA) - }, - ) - val indicator = rememberLayeredComponent( - rear = indicatorRearComponent, - front = rememberLayeredComponent( - rear = indicatorCenterComponent, - front = indicatorFrontComponent, - padding = indicatorPadding, - ), - padding = indicatorPadding, - ) - val guideline = rememberUnboundedLineComponent( - color = color, - verticalAddDrawSpace = TangemTheme.dimens.spacing24, - shape = remember { Shape.dashed(Shape.Rectangle, 4.dp, 4.dp) }, - ) - return remember(indicator, guideline) { - object : DefaultCartesianMarker( - label = TextComponent.build { textSizeSp = 0f }, - indicator = indicator, - indicatorSizeDp = INDICATOR_SIZE_DP, - guideline = guideline, - ) { - override fun getInsets( - context: CartesianMeasureContext, - outInsets: Insets, - horizontalDimensions: HorizontalDimensions, - ) { - with(context) { - super.getInsets(context, outInsets, horizontalDimensions) - val baseShadowInsetDp = - CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER * LABEL_BACKGROUND_SHADOW_RADIUS_DP - outInsets.top += (baseShadowInsetDp - LABEL_BACKGROUND_SHADOW_DY_DP).pixels - outInsets.bottom += (baseShadowInsetDp + LABEL_BACKGROUND_SHADOW_DY_DP).pixels - } - } - } - } -} - -private val indicatorPadding = Dimensions.of(3.dp) -private const val LABEL_BACKGROUND_SHADOW_RADIUS_DP = 4f -private const val LABEL_BACKGROUND_SHADOW_DY_DP = 2f -private const val CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER = 1.4f -private const val INDICATOR_SIZE_DP = 16f -private const val INDICATOR_REAR_COLOR_ALPHA = .24f - -// region Preview - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun TangemChartMarkerPreview( - @PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair, List>, -) { - val marker = rememberTangemChartMarker(Color.Red, Color.White) - val y = previewData.second.map { it.toFloat() } - val x = List(y.size) { it.toFloat() } - val model = CartesianChartModel(LineCartesianLayerModel.build { series(x, y) }) - - val centerAprx = (model.models[0].minX + model.models[0].maxX) / 2f - val center = model.models[0].getXDeltaGcd().let { centerAprx - centerAprx % it } - - TangemThemePreview { - Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) { - CartesianChartHost( - modifier = Modifier.fillMaxWidth(), - chart = rememberCartesianChart( - rememberLineCartesianLayer( - listOf(rememberLineSpec(shader = DynamicShader.color(Color.Blue))), - axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), - ), - persistentMarkers = mapOf(center to marker), - ), - model = model, - marker = marker, - zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), - horizontalLayout = HorizontalLayout.fullWidth(), - ) - } - } -} - -// endregion Preview \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt index 7e4887fedc..44f973dc73 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt @@ -1,6 +1,8 @@ package com.tangem.common.ui.charts.state import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal @Immutable @@ -30,7 +32,7 @@ sealed interface MarketChartData { */ @Immutable data class Data( - val x: List = listOf(), - val y: List = listOf(), + val x: ImmutableList = persistentListOf(), + val y: ImmutableList = persistentListOf(), ) : MarketChartData } \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt index bcad7d3ceb..86f5ef5dd6 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt @@ -3,13 +3,13 @@ package com.tangem.common.ui.charts.state import androidx.compose.runtime.Stable import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel -import com.patrykandpatrick.vico.core.common.data.ExtraStore -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow +import com.tangem.common.ui.charts.state.converter.PointValuesConverter +import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter +import com.tangem.common.ui.charts.state.formatter.FormatterWrapWithCache +import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.withContext -import java.math.BigDecimal +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock /** * This class represents a transaction for updating the state and look of a Market Chart. @@ -25,7 +25,12 @@ class Transaction( var chartData: MarketChartData.NoData? = null fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) { - chartLook = block(currentLook) + val newLook = block(currentLook) + + chartLook = newLook.copy( + xAxisFormatter = FormatterWrapWithCache(newLook.xAxisFormatter), + yAxisFormatter = FormatterWrapWithCache(newLook.yAxisFormatter), + ) } fun updateState(block: (prev: MarketChartData) -> MarketChartData.NoData) { @@ -56,7 +61,12 @@ class TransactionSuspend( } fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) { - chartLook = block(currentLook) + val newLook = block(currentLook) + + chartLook = newLook.copy( + xAxisFormatter = FormatterWrapWithCache(newLook.xAxisFormatter), + yAxisFormatter = FormatterWrapWithCache(newLook.yAxisFormatter), + ) } internal fun updateState(block: (prev: MarketChartData) -> MarketChartData) { @@ -75,21 +85,24 @@ class TransactionSuspend( class MarketChartDataProducer private constructor( initialData: MarketChartData, initialLook: MarketChartLook, - val pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter, + val pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = true), private val dispatcher: CoroutineDispatcher = Dispatchers.Default, ) { - internal val startDrawingAnimation = MutableSharedFlow() internal val dataState = MutableStateFlow(initialData) internal val lookState = MutableStateFlow(initialLook) internal val entries = MutableStateFlow>(emptyList()) - - internal val modelProducer = CartesianChartModelProducer.build(dispatcher = dispatcher) + internal val modelProducer = CartesianChartModelProducer(dispatcher = dispatcher) + internal val rawData = MutableStateFlow(null) + private val mutex = Mutex() /** * This function runs a suspending transaction block to update the state and look of the Market Chart. */ - suspend fun runTransactionSuspend(block: TransactionSuspend.() -> Unit) = - handleTransactionSuspend(transaction = TransactionSuspend(dataState.value, lookState.value).apply(block)) + suspend fun runTransactionSuspend(block: TransactionSuspend.() -> Unit) = withContext(dispatcher) { + mutex.withLock { + handleTransactionSuspend(transaction = TransactionSuspend(dataState.value, lookState.value).apply(block)) + } + } /** * This function runs a non-suspending transaction block to update the state and look of the Market Chart. @@ -102,32 +115,30 @@ class MarketChartDataProducer private constructor( val chartData = transaction.chartData val oldData = dataState.value - if (chartData != null) { - dataState.value = chartData - } - if (chartData is MarketChartData.Data && (oldData !is MarketChartData.Data || oldData != chartData)) { - if (lookState.value.animationOnDataChange) { - startDrawingAnimation.emit(Unit) - } - withContext(dispatcher) { - val rawData = pointsValuesConverter.convert(chartData) + (lookState.value.xAxisFormatter as? FormatterWrapWithCache)?.clearCache() + (lookState.value.yAxisFormatter as? FormatterWrapWithCache)?.clearCache() - val entriesLocal = - rawData.x.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, rawData.y[index]) } + val rawData = pointsValuesConverter.convert(chartData) - entries.value = entriesLocal + val entriesLocal = + rawData.x.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, rawData.y[index]) } + currentCoroutineContext().ensureActive() + + runCatching { modelProducer.runTransaction { add(LineCartesianLayerModel.Partial(series = listOf(entriesLocal))) - - updateExtras { - it[entriesKey] = entriesLocal - it[xKey] = chartData.x - it[yKey] = chartData.y - } - }.await() + } } + + entries.value = entriesLocal + dataState.value = chartData + this.rawData.value = rawData + + delay(timeMillis = 200) + } else if (chartData != null) { + dataState.value = chartData } nonSuspendTransaction?.let { handleTransaction(it) } @@ -143,10 +154,6 @@ class MarketChartDataProducer private constructor( } companion object { - internal val entriesKey = ExtraStore.Key>() - internal val xKey = ExtraStore.Key>() - internal val yKey = ExtraStore.Key>() - private val initialData: MarketChartData = MarketChartData.NoData.Empty private val initialLook: MarketChartLook = MarketChartLook() @@ -159,7 +166,7 @@ class MarketChartDataProducer private constructor( * @return A MarketChartDataProducer. */ suspend fun buildSuspend( - pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter, + pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = true), dispatcher: CoroutineDispatcher = Dispatchers.Default, block: TransactionSuspend.() -> Unit, ): MarketChartDataProducer { @@ -184,7 +191,7 @@ class MarketChartDataProducer private constructor( * @return A MarketChartDataProducer. */ fun build( - pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter, + pointsValuesConverter: PointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = true), dispatcher: CoroutineDispatcher = Dispatchers.Default, block: Transaction.() -> Unit, ): MarketChartDataProducer { diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt index c9ecaae2c0..dc194c3b3b 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt @@ -1,5 +1,8 @@ package com.tangem.common.ui.charts.state +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.charts.state.formatter.AxisLabelFormatter + /** * This class represents the look and feel of a Market Chart. * It includes properties for type, marker highlight, animation on data change, animate data appearance, @@ -7,16 +10,13 @@ package com.tangem.common.ui.charts.state * * @property type The type of the chart, can be either Growing or Falling. * @property markerHighlightRightSide A boolean indicating whether the marker highlights the right side of the chart. - * @property animationOnDataChange A boolean indicating whether to animate on data change. - * @property animateDataAppearance A boolean indicating whether to animate data appearance. * @property xAxisFormatter A formatter for the x-axis labels. * @property yAxisFormatter A formatter for the y-axis labels. */ +@Immutable data class MarketChartLook( val type: Type = Type.Growing, val markerHighlightRightSide: Boolean = true, - val animationOnDataChange: Boolean = false, - val animateDataAppearance: Boolean = false, val xAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() }, val yAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() }, ) { diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartRawData.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartRawData.kt index a1b7e91f2c..df6d124201 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartRawData.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartRawData.kt @@ -1,9 +1,20 @@ package com.tangem.common.ui.charts.state import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +/** + * This class represents raw data for a Market Chart. Used for drawing the chart. + * + * @property originalIndexes If the source data has the original representation (due to reduced sampling), + * this list contains the original indexes of the data points. + * @property y The list of y-values. + * @property x The list of x-values. + */ @Immutable data class MarketChartRawData( - val y: List, - val x: List = List(y.size) { 1f }, + val originalIndexes: ImmutableList? = null, + val y: ImmutableList, + val x: ImmutableList = List(y.size) { 1.0 }.toImmutableList(), ) \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt index 92a0379f5d..8db48deab6 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt @@ -2,7 +2,6 @@ package com.tangem.common.ui.charts.state import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener @@ -20,26 +19,22 @@ import java.math.BigDecimal @Composable fun rememberMarketChartState( dataProducer: MarketChartDataProducer = remember { MarketChartDataProducer.build {} }, - colorMapper: (MarketChartLook.Type) -> Color = { - when (it) { - MarketChartLook.Type.Growing -> Color.Green - MarketChartLook.Type.Falling -> Color.Red + colorMapper: (MarketChartLook.Type) -> Color = remember { + { + when (it) { + MarketChartLook.Type.Growing -> Color.Green + MarketChartLook.Type.Falling -> Color.Red + } } }, onMarkerShown: (x: BigDecimal?, y: BigDecimal?) -> Unit = { _, _ -> }, ): MarketChartState { - val lookState = dataProducer.lookState.collectAsStateWithLifecycle() + val lookState = dataProducer.lookState.collectAsState() val state = remember(dataProducer, lookState, colorMapper, onMarkerShown) { MarketChartState(dataProducer, lookState, colorMapper, onMarkerShown) } - LaunchedEffect(Unit) { - dataProducer.startDrawingAnimation.collect { - state.startDrawingAnimation() - } - } - return state } @@ -59,7 +54,6 @@ class MarketChartState internal constructor( private val colorMapper: (MarketChartLook.Type) -> Color, private val markerCallback: (x: BigDecimal?, y: BigDecimal?) -> Unit, ) { - internal val startDrawingAnimationState = mutableStateOf(false) internal val modelProducer = dataProducer.modelProducer internal val chartColor by derivedStateOf { @@ -70,29 +64,29 @@ class MarketChartState internal constructor( lookState.value.markerHighlightRightSide } - internal val xValueFormatter by derivedStateOf { - CartesianValueFormatter { value, _, _ -> - val state = dataProducer.dataState.value as? MarketChartData.Data - ?: return@CartesianValueFormatter value.toString() + internal val xValueFormatter = CartesianValueFormatter { value, _, _ -> + val formatter = dataProducer.lookState.value.xAxisFormatter - lookState.value.xAxisFormatter.format( - value = dataProducer.pointsValuesConverter.prepareRawXForFormat(value, state), - ) - } + val state = dataProducer.dataState.value as? MarketChartData.Data + ?: return@CartesianValueFormatter value.toString() + + formatter.format( + value = dataProducer.pointsValuesConverter.prepareRawXForFormat(value, state), + ) } - internal val yValueFormatter by derivedStateOf { - CartesianValueFormatter { value, _, _ -> - val state = dataProducer.dataState.value as? MarketChartData.Data - ?: return@CartesianValueFormatter value.toString() + internal val yValueFormatter = CartesianValueFormatter { value, _, _ -> + val formatter = dataProducer.lookState.value.yAxisFormatter - lookState.value.yAxisFormatter.format( - value = dataProducer.pointsValuesConverter.prepareRawYForFormat(value, state), - ) - } + val state = dataProducer.dataState.value as? MarketChartData.Data + ?: return@CartesianValueFormatter value.toString() + + formatter.format( + value = dataProducer.pointsValuesConverter.prepareRawYForFormat(value, state), + ) } - internal var markerFraction: Float? by mutableStateOf(null) + internal var markerFraction by mutableStateOf(null) internal val markerVisibilityListener = object : CartesianMarkerVisibilityListener { override fun onShown(marker: CartesianMarker, targets: List) { @@ -116,24 +110,17 @@ class MarketChartState internal constructor( } } - val isDrawingAnimationInProgress: Boolean by derivedStateOf { - startDrawingAnimationState.value - } - private fun getPoint(targets: List): Pair? { val entry = (targets[0] as LineCartesianLayerMarkerTarget).points[0].entry val entryIndex = dataProducer.entries.value.indexOf(entry).takeIf { it != -1 } ?: return null val state = dataProducer.dataState.value as? MarketChartData.Data ?: return null - val x = state.x.getOrNull(entryIndex) ?: return null - val y = state.y.getOrNull(entryIndex) ?: return null + val rawData = dataProducer.rawData.value ?: return null + + val originalIndex = rawData.originalIndexes?.getOrNull(entryIndex) + val index = originalIndex ?: entryIndex + + val x = state.x.getOrNull(index) ?: return null + val y = state.y.getOrNull(index) ?: return null return x to y } - - fun startDrawingAnimation() { - startDrawingAnimationState.value = true - } - - fun stopDrawingAnimation() { - startDrawingAnimationState.value = false - } } \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/PointValuesConverter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/PointValuesConverter.kt deleted file mode 100644 index 1dab1cd0cc..0000000000 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/PointValuesConverter.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.tangem.common.ui.charts.state - -import java.math.BigDecimal - -/** - * Interface to convert chart data values to Floats and backwards. - * - * We need to convert the values on the graph to floating point values in order to display them correctly on the canvas. - * We also need to determine exactly which floating point value on the graph corresponds to the decimal point, - * so that we can format the actual value and display on the x/y axis. - */ -interface PointValuesConverter { - - fun convert(data: MarketChartData.Data): MarketChartRawData - - fun prepareRawXForFormat(rawX: Float, data: MarketChartData.Data): BigDecimal - - fun prepareRawYForFormat(rawY: Float, data: MarketChartData.Data): BigDecimal -} - -object DefaultPointValuesConverter : PointValuesConverter { - - override fun convert(data: MarketChartData.Data): MarketChartRawData { - val minX = data.x.min() - val minY = data.y.min() - - val normY = data.y.map { normalize(it, minY) } - val normX = data.x.map { normalize(it, minX) } - - return MarketChartRawData( - x = normX, - y = normY, - ) - } - - override fun prepareRawXForFormat(rawX: Float, data: MarketChartData.Data): BigDecimal { - val dataMin = data.x.min() - val scale = dataMin.scale() - val bVal = if (scale > 2) { - rawX.toBigDecimal().movePointLeft(scale - 2) + dataMin - } else { - rawX.toBigDecimal() + dataMin - } - - return bVal - } - - override fun prepareRawYForFormat(rawY: Float, data: MarketChartData.Data): BigDecimal { - val dataMin = data.y.min() - val scale = dataMin.scale() - val bVal = if (scale > 2) { - rawY.toBigDecimal().movePointLeft(scale - 2) + dataMin - } else { - rawY.toBigDecimal() + dataMin - } - - return bVal - } - - // TODO enhance algorithm for values with big difference between min and max, which cannot fit in Float - private fun normalize(value: BigDecimal, min: BigDecimal, scale: Int = min.scale()): Float { - val n = value - min - return if (scale > 2) { - n.movePointRight(scale - 2).toFloat() - } else { - n.toFloat() - } - } -} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PointValuesConverter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PointValuesConverter.kt new file mode 100644 index 0000000000..107eef4c68 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PointValuesConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.common.ui.charts.state.converter + +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.MarketChartRawData +import java.math.BigDecimal + +/** + * Interface to convert chart data values to Floats and backwards. + * + * We need to convert the values on the graph to floating point values in order to display them correctly on the canvas. + * We also need to determine exactly which floating point value on the graph corresponds to the decimal point, + * so that we can format the actual value and display on the x/y axis. + * + * **[prepareRawXForFormat] and [prepareRawYForFormat] must be very fast because they are called in the onDraw method** + */ +interface PointValuesConverter { + + fun convert(data: MarketChartData.Data): MarketChartRawData + + fun prepareRawXForFormat(rawX: Double, data: MarketChartData.Data): BigDecimal + + fun prepareRawYForFormat(rawY: Double, data: MarketChartData.Data): BigDecimal +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt new file mode 100644 index 0000000000..e51a8f72a6 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/converter/PriceAndTimePointValuesConverter.kt @@ -0,0 +1,108 @@ +package com.tangem.common.ui.charts.state.converter + +import com.tangem.common.ui.charts.downsample.LTThreeBuckets +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.MarketChartRawData +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal + +@Suppress("MagicNumber") +class PriceAndTimePointValuesConverter( + private val needToFormatAxis: Boolean, +) : PointValuesConverter { + + private data class MinMaxCache( + val minX: BigDecimal, + val maxX: BigDecimal, + val minY: BigDecimal, + val maxY: BigDecimal, + ) + + private var minMaxCache = MinMaxCache(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO) + private val formatYValuesCache = mutableMapOf() + private val formatXValuesCache = mutableMapOf() + + override fun convert(data: MarketChartData.Data): MarketChartRawData { + formatYValuesCache.clear() + formatXValuesCache.clear() + val cache = MinMaxCache( + minY = data.y.minOrNull() ?: BigDecimal.ZERO, + maxY = data.y.maxOrNull() ?: BigDecimal.ZERO, + minX = data.x.minOrNull() ?: BigDecimal.ZERO, + maxX = data.x.maxOrNull() ?: BigDecimal.ZERO, + ) + minMaxCache = cache + + val normY = data.y.normalizeToDouble(min = cache.minY, max = cache.maxY) + val normX = data.x.normalizeTime(min = cache.minX, max = cache.maxX) + + return if (normX.size > MAX_POINTS) { + LTThreeBuckets + .downsample(normX, normY, MAX_POINTS - 2) + .let { + MarketChartRawData( + originalIndexes = it.originalIndexes.toImmutableList(), + x = it.x.toImmutableList(), + y = it.y.toImmutableList(), + ) + } + } else { + MarketChartRawData( + x = normX.toImmutableList(), + y = normY.toImmutableList(), + ) + } + } + + override fun prepareRawXForFormat(rawX: Double, data: MarketChartData.Data): BigDecimal { + if (!needToFormatAxis) return BigDecimal.ZERO + if (formatXValuesCache.containsKey(rawX)) return formatXValuesCache[rawX]!! + + val result = (rawX * MINUTE).toBigDecimal() + + formatXValuesCache[rawX] = result + return result + } + + override fun prepareRawYForFormat(rawY: Double, data: MarketChartData.Data): BigDecimal { + if (!needToFormatAxis) return BigDecimal.ZERO + if (formatYValuesCache.containsKey(rawY)) return formatYValuesCache[rawY]!! + + val min = minMaxCache.minY + val max = minMaxCache.maxY + val length = max - min + + val result = when { + rawY < 0.01f -> min + rawY < 0.55f && rawY > 0.45f -> min + length / 2.toBigDecimal() + rawY > 0.97f && rawY < 1.01f -> max + else -> length * rawY.toBigDecimal() + min + } + formatYValuesCache[rawY] = result + return result + } + + private fun List.normalizeToDouble(min: BigDecimal, max: BigDecimal): List { + if (min == max) { + return List(size) { 0.5 } + } + + return map { ((it - min) / (max - min)).toDouble() } + } + + private fun List.normalizeTime(min: BigDecimal, max: BigDecimal): List { + if (min == max) { + return List(size) { 0.5 } + } + + return map { + (it / MINUTE_BIG).toDouble() + } + } + + private companion object { + private const val MAX_POINTS = 502 + private const val MINUTE = 60000L + private val MINUTE_BIG = 60000L.toBigDecimal() + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/formatter/AxisLabelFormatter.kt similarity index 76% rename from common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt rename to common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/formatter/AxisLabelFormatter.kt index e01681994c..46feae32c2 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/formatter/AxisLabelFormatter.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.charts.state +package com.tangem.common.ui.charts.state.formatter import androidx.compose.runtime.Stable import java.math.BigDecimal @@ -7,6 +7,8 @@ import java.math.BigDecimal * Used for formatting the axis labels in a chart. * It takes a BigDecimal value and returns a CharSequence that represents the formatted label. * + * [format] has to be very fast because it is called in the onDraw method. + * * @param value The value to be formatted. * @return The formatted label as a CharSequence. */ diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/formatter/FormatterWrapWithCache.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/formatter/FormatterWrapWithCache.kt new file mode 100644 index 0000000000..b0a0fbe8bc --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/formatter/FormatterWrapWithCache.kt @@ -0,0 +1,15 @@ +package com.tangem.common.ui.charts.state.formatter + +import java.math.BigDecimal + +internal class FormatterWrapWithCache(private val formatter: AxisLabelFormatter) : AxisLabelFormatter { + private val cache = mutableMapOf() + + override fun format(value: BigDecimal): CharSequence { + return cache.getOrPut(value) { formatter.format(value) } + } + + fun clearCache() { + cache.clear() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt index 014f9adf75..d09e293151 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -28,6 +28,7 @@ interface TangemTechMarketsApi { @GET("coins/{coin_id}/history") suspend fun getCoinChart( + @Path("coin_id") coinId: String, @Query("currency") currency: String, @Query("interval") interval: String, ): ApiResponse diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 300231f9c3..f62345873b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -56,6 +56,7 @@ fun TangemTheme( ) { CompositionLocalProvider( LocalTangemShimmer provides TangemShimmer, + LocalMainBottomSheetColor provides remember { mutableStateOf(Color.Unspecified) }, ) { ProvideTextStyle( value = TangemTheme.typography.body1, @@ -246,4 +247,8 @@ val LocalWindowSize = staticCompositionLocalOf { val LocalTangemShimmer = staticCompositionLocalOf { error("No TangemShimmer provided") +} + +val LocalMainBottomSheetColor = staticCompositionLocalOf> { + error("No MainBottomSheetColor provided") } \ 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 d42121d3f0..7c6fa15b74 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 @@ -61,7 +61,14 @@ object DateTimeFormatters { */ val dateMMMMd: DateTimeFormatter by lazy { DateTimeFormatterBuilder() - .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d")) + .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "dd MMM")) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + val dateYYYY: DateTimeFormatter by lazy { + DateTimeFormatterBuilder() + .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyy")) .toFormatter() .withLocale(Locale.getDefault()) } diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index eda05fed37..ce15e2c6fe 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -91,9 +91,14 @@ internal class DefaultMarketsTokenRepository( ).toBatchFlow() } - override suspend fun getChart(interval: PriceChangeInterval, tokenId: String): TokenChart { + override suspend fun getChart( + fiatCurrencyCode: String, + interval: PriceChangeInterval, + tokenId: String, + ): TokenChart { val response = marketsApi.getCoinChart( - currency = tokenId, + currency = fiatCurrencyCode, + coinId = tokenId, interval = interval.toRequestParam(), ) diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt index f47c0ff170..eee63ef4e9 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt @@ -20,7 +20,7 @@ fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) { fun PriceChangeInterval.toRequestParam(): String = when (this) { PriceChangeInterval.H24 -> "24h" PriceChangeInterval.WEEK -> "1w" - PriceChangeInterval.MONTH -> "30d" + PriceChangeInterval.MONTH -> "1m" PriceChangeInterval.MONTH3 -> "3m" PriceChangeInterval.MONTH6 -> "6m" PriceChangeInterval.YEAR -> "1y" diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt index 4eb118ab29..88eccb1163 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt @@ -3,6 +3,7 @@ package com.tangem.data.markets.converters import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenMarketListConfig class TokenMarketChartsConverter( private val tokenChartConverter: TokenChartConverter, @@ -11,23 +12,29 @@ class TokenMarketChartsConverter( fun convert( chartsToCopy: TokenMarket.Charts, tokenId: String, - interval: PriceChangeInterval, + interval: TokenMarketListConfig.Interval, value: TokenMarketChartListResponse, ): TokenMarket.Charts { val prices = requireNotNull(value[tokenId]) { "$tokenId is not found in the response. This shouldn't have happened." } return when (interval) { - PriceChangeInterval.H24 -> chartsToCopy.copy( - h24 = tokenChartConverter.convert(interval, prices), + TokenMarketListConfig.Interval.H24 -> chartsToCopy.copy( + h24 = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices), ) - PriceChangeInterval.WEEK -> chartsToCopy.copy( - week = tokenChartConverter.convert(interval, prices), + TokenMarketListConfig.Interval.WEEK -> chartsToCopy.copy( + week = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices), ) - PriceChangeInterval.MONTH -> chartsToCopy.copy( - month = tokenChartConverter.convert(interval, prices), + TokenMarketListConfig.Interval.MONTH -> chartsToCopy.copy( + month = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices), ) else -> error("unsupported interval=$interval. This shouldn't have happened.") } } + + private fun TokenMarketListConfig.Interval.toPriceChangeInterval(): PriceChangeInterval = when (this) { + TokenMarketListConfig.Interval.H24 -> PriceChangeInterval.H24 + TokenMarketListConfig.Interval.WEEK -> PriceChangeInterval.WEEK + TokenMarketListConfig.Interval.MONTH -> PriceChangeInterval.MONTH + } } \ No newline at end of file diff --git a/domain/app-currency/models/build.gradle.kts b/domain/app-currency/models/build.gradle.kts index 7ff7fb7522..6b18f3f83f 100644 --- a/domain/app-currency/models/build.gradle.kts +++ b/domain/app-currency/models/build.gradle.kts @@ -1,4 +1,9 @@ plugins { alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) id("configuration") +} + +dependencies { + implementation(deps.kotlin.serialization) } \ No newline at end of file diff --git a/domain/app-currency/models/src/main/kotlin/com/tangem/domain/appcurrency/model/AppCurrency.kt b/domain/app-currency/models/src/main/kotlin/com/tangem/domain/appcurrency/model/AppCurrency.kt index 9d5261347a..adadcdcbac 100644 --- a/domain/app-currency/models/src/main/kotlin/com/tangem/domain/appcurrency/model/AppCurrency.kt +++ b/domain/app-currency/models/src/main/kotlin/com/tangem/domain/appcurrency/model/AppCurrency.kt @@ -1,5 +1,8 @@ package com.tangem.domain.appcurrency.model +import kotlinx.serialization.Serializable + +@Serializable data class AppCurrency( val code: String, val name: String, diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index 5529922be2..8545d1c3b3 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -11,9 +11,10 @@ android { dependencies { - api(projects.domain.markets.models) + api(projects.domain.appCurrency.models) api(projects.domain.core) api(projects.core.pagination) + api(projects.domain.markets.models) implementation(deps.kotlin.serialization) implementation(projects.domain.tokens.models) diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt index bcac52fddb..a23a9136ba 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt @@ -7,7 +7,7 @@ sealed class TokenMarketUpdateRequest { ) : TokenMarketUpdateRequest() data class UpdateChart( - val interval: PriceChangeInterval, + val interval: TokenMarketListConfig.Interval, val currency: String, ) : TokenMarketUpdateRequest() } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt index e2db0be0d3..c686ff3874 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt @@ -1,15 +1,24 @@ package com.tangem.domain.markets import arrow.core.Either +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.repositories.MarketsTokenRepository class GetTokenPriceChartUseCase( private val marketsTokenRepository: MarketsTokenRepository, ) { - suspend operator fun invoke(interval: PriceChangeInterval, tokenId: String): Either { + suspend operator fun invoke( + appCurrency: AppCurrency, + interval: PriceChangeInterval, + tokenId: String, + ): Either { return Either.catch { - marketsTokenRepository.getChart(interval = interval, tokenId = tokenId) + marketsTokenRepository.getChart( + fiatCurrencyCode = appCurrency.code, + interval = interval, + tokenId = tokenId, + ) }.mapLeft {} } } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index d6cb5a094b..8b3ce43ce5 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -10,5 +10,5 @@ interface MarketsTokenRepository { nextBatchSize: Int, ): TokenListBatchFlow - suspend fun getChart(interval: PriceChangeInterval, tokenId: String): TokenChart + suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt index 987d11c0a8..0078651a11 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt @@ -1,36 +1,38 @@ package com.tangem.features.markets -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State +import androidx.compose.animation.Animatable +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp +import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.router.stack.StackNavigation -import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.router.stack.push +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.* +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.stack.* import com.arkivanov.decompose.value.Value import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.component.BottomSheetState import com.tangem.features.markets.component.MarketsEntryComponent import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent import com.tangem.features.markets.details.api.toSerializable import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent -import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +@Stable internal class DefaultMarketsEntryComponent @AssistedInject constructor( @Assisted context: AppComponentContext, private val marketsEntryChildFactory: MarketsEntryChildFactory, ) : MarketsEntryComponent, AppComponentContext by context { - private val model: MarketsListModel = getOrCreateModel() - private val stackNavigation = StackNavigation() val stack: Value> = childStack( @@ -44,20 +46,36 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor( child = configuration, appComponentContext = childByContext(componentContext), onTokenSelected = ::marketsListTokenSelected, + onDetailsBack = ::onDetailsBack, ) }, ) + @Suppress("LongMethod") @Composable override fun BottomSheetContent( bottomSheetState: State, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier, ) { - Children(stack) { + val primary = TangemTheme.colors.background.primary + val secondary = TangemTheme.colors.background.secondary + val backgroundColor = remember { Animatable(primary) } + val stackState = stack.subscribeAsState() + + LocalMainBottomSheetColor.current.value = backgroundColor.value + + Children( + stack = stackState.value, + animation = stackAnimation(slide()), + ) { when (it.configuration) { is MarketsEntryChildFactory.Child.TokenDetails -> { - (it.instance as MarketsTokenDetailsComponent).Content(modifier) + (it.instance as MarketsTokenDetailsComponent).BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) } MarketsEntryChildFactory.Child.TokenList -> { (it.instance as MarketsTokenListComponent).BottomSheetContent( @@ -68,16 +86,75 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor( } } } + + val activeChild = stackState.value.active.configuration + + LaunchedEffect(bottomSheetState.value) { + if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) { + when (bottomSheetState.value) { + BottomSheetState.EXPANDED -> { + backgroundColor.animateTo( + secondary, + animationSpec = tween(durationMillis = 100), + ) + } + BottomSheetState.COLLAPSED -> { + backgroundColor.animateTo( + primary, + animationSpec = tween(durationMillis = 100), + ) + } + } + } + } + + LaunchedEffect(activeChild) { + when (activeChild) { + is MarketsEntryChildFactory.Child.TokenDetails -> { + backgroundColor.animateTo( + secondary, + animationSpec = tween(durationMillis = 500), + ) + } + MarketsEntryChildFactory.Child.TokenList -> { + backgroundColor.animateTo( + primary, + animationSpec = tween(durationMillis = 500), + ) + } + } + } + + LaunchedEffect(primary, secondary) { + if (backgroundColor.isRunning) return@LaunchedEffect + + when (activeChild) { + is MarketsEntryChildFactory.Child.TokenDetails -> { + backgroundColor.snapTo(secondary) + } + MarketsEntryChildFactory.Child.TokenList -> { + backgroundColor.snapTo(primary) + } + } + } } - private fun marketsListTokenSelected(token: TokenMarket) { - stackNavigation.push( + @OptIn(ExperimentalDecomposeApi::class) + private fun marketsListTokenSelected(token: TokenMarket, appCurrency: AppCurrency) { + stackNavigation.pushNew( configuration = MarketsEntryChildFactory.Child.TokenDetails( - params = MarketsTokenDetailsComponent.Params(token.toSerializable()), + params = MarketsTokenDetailsComponent.Params( + token = token.toSerializable(), + appCurrency = appCurrency, + ), ), ) } + private fun onDetailsBack() { + stackNavigation.popWhile { it != MarketsEntryChildFactory.Child.TokenList } + } + @AssistedFactory interface Factory : MarketsEntryComponent.Factory { override fun create(context: AppComponentContext): DefaultMarketsEntryComponent diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/MarketsEntryChildFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/MarketsEntryChildFactory.kt index 9ad07ec122..0db378d0f5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/MarketsEntryChildFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/MarketsEntryChildFactory.kt @@ -1,6 +1,8 @@ package com.tangem.features.markets +import androidx.compose.runtime.Immutable import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent @@ -13,25 +15,30 @@ internal class MarketsEntryChildFactory @Inject constructor( ) { @Serializable + @Immutable sealed interface Child { @Serializable + @Immutable data object TokenList : Child @Serializable + @Immutable data class TokenDetails(val params: MarketsTokenDetailsComponent.Params) : Child } fun createChild( child: Child, appComponentContext: AppComponentContext, - onTokenSelected: (TokenMarket) -> Unit, + onTokenSelected: (TokenMarket, AppCurrency) -> Unit, + onDetailsBack: () -> Unit, ): Any { return when (child) { is Child.TokenDetails -> { tokenDetailsComponentFactory.create( context = appComponentContext, params = child.params, + onBack = onDetailsBack, ) } is Child.TokenList -> { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/MarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/MarketsTokenDetailsComponent.kt index 74407f6910..a706e5850e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/MarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/MarketsTokenDetailsComponent.kt @@ -1,15 +1,32 @@ package com.tangem.features.markets.details.api +import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.markets.component.BottomSheetState import kotlinx.serialization.Serializable @Stable -interface MarketsTokenDetailsComponent : ComposableContentComponent { +interface MarketsTokenDetailsComponent { @Serializable - data class Params(val token: TokenMarketSerializable) + data class Params( + val token: TokenMarketSerializable, + val appCurrency: AppCurrency, + ) - interface Factory : ComponentFactory + @Composable + fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) + + interface Factory { + fun create(context: AppComponentContext, params: Params, onBack: () -> Unit): MarketsTokenDetailsComponent + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt index ad994d8f70..5bc189be06 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -2,11 +2,14 @@ package com.tangem.features.markets.details.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.markets.component.BottomSheetState import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent @@ -18,17 +21,23 @@ import dagger.assisted.AssistedInject internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: MarketsTokenDetailsComponent.Params, + @Assisted private val onBack: () -> Unit, ) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent { private val model: MarketsTokenDetailsModel = getOrCreateModel(params) @Composable - override fun Content(modifier: Modifier) { + override fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) { val state by model.state.collectAsStateWithLifecycle() MarketsTokenDetailsContent( state = state, - onBackClick = {}, + onBackClick = { onBack() }, + onHeaderSizeChange = onHeaderSizeChange, modifier = modifier, ) } @@ -38,6 +47,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( override fun create( context: AppComponentContext, params: MarketsTokenDetailsComponent.Params, + onBack: () -> Unit, ): DefaultMarketsTokenDetailsComponent } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index e2bece6eca..f1814f03df 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -1,60 +1,254 @@ package com.tangem.features.markets.details.impl.model -import com.tangem.common.ui.charts.state.MarketChartData -import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.common.ui.charts.state.MarketChartLook +import androidx.compose.runtime.Stable +import arrow.core.getOrElse +import com.tangem.common.ui.charts.state.* import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetTokenPriceChartUseCase import com.tangem.domain.markets.PriceChangeInterval import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM +import com.tangem.features.markets.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import java.math.BigDecimal +import java.math.RoundingMode import javax.inject.Inject +@Stable internal class MarketsTokenDetailsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, ) : Model() { val params = paramsContainer.require() + private val currentAppCurrency = getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + }.stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = params.appCurrency, + ) + private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) { chartData = MarketChartData.NoData.Loading + + updateLook { + it.copy( + type = getChartTypeByPercent(params.token.tokenQuotes.h24Percent), + xAxisFormatter = { value -> + value.toLong().toTimeFormat(DateTimeFormatters.timeFormatter) + }, + yAxisFormatter = { value -> + BigDecimalFormatter.formatFiatAmountUncapped( + fiatAmount = value, + fiatCurrencyCode = currentAppCurrency.value.code, + fiatCurrencySymbol = "", + ) + }, + ) + } } val state = MutableStateFlow( MarketsTokenDetailsUM( tokenName = params.token.name, - dateTimeText = "datetime", - priceChangePercentText = params.token.tokenQuotes.h24Percent.toString(), + priceText = BigDecimalFormatter.formatFiatAmountUncapped( + fiatAmount = params.token.tokenQuotes.currentPrice, + fiatCurrencyCode = currentAppCurrency.value.code, + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ), + dateTimeText = resourceReference(R.string.common_today), + priceChangePercentText = BigDecimalFormatter.formatPercent( + percent = params.token.tokenQuotes.h24Percent, + useAbsoluteValue = true, + ), priceChangeType = if (params.token.tokenQuotes.h24Percent < BigDecimal.ZERO) { PriceChangeType.DOWN } else { PriceChangeType.UP }, + iconUrl = params.token.imageUrl, chartState = MarketsTokenDetailsUM.ChartState( dataProducer = chartDataProducer, chartLook = MarketChartLook(), onLoadRetryClick = {}, + status = MarketsTokenDetailsUM.ChartState.Status.LOADING, + onMarkerPointSelected = ::onMarkerPointSelected, ), + selectedInterval = PriceChangeInterval.H24, + onSelectedIntervalChange = ::onSelectedIntervalChange, ), ) + private val loadChartJobHolder = JobHolder() + init { + loadChart(PriceChangeInterval.H24) + } + + private fun onSelectedIntervalChange(interval: PriceChangeInterval) { + if (state.value.selectedInterval == interval) return + + state.update { + it.copy( + selectedInterval = interval, + priceChangeType = PriceChangeType.UP, + ) + } + + loadChart(interval) + } + + private fun loadChart(interval: PriceChangeInterval) { modelScope.launch { - val chart = getTokenPriceChartUseCase.invoke(PriceChangeInterval.H24, params.token.id) + state.update { + it.copy( + chartState = it.chartState.copy( + status = MarketsTokenDetailsUM.ChartState.Status.LOADING, + ), + ) + } + + chartDataProducer.runTransactionSuspend { + chartData = MarketChartData.NoData.Loading + } + + val chart = getTokenPriceChartUseCase.invoke( + appCurrency = currentAppCurrency.value, + interval = interval, + tokenId = params.token.id, + ) + + state.update { + it.copy( + selectedInterval = interval, + chartState = it.chartState.copy( + status = MarketsTokenDetailsUM.ChartState.Status.LOADING, + ), + ) + } + + val xAxisFormatter = getFormatterByInterval(state.value.selectedInterval) chart.onRight { chartDataProducer.runTransactionSuspend { - chartData = MarketChartData.Data() + chartData = MarketChartData.Data( + x = it.timeStamps.map { it.toBigDecimal() }.toImmutableList(), + y = it.priceY.toImmutableList(), + ) + + updateLook { + it.copy( + xAxisFormatter = xAxisFormatter, + ) + } } + + state.update { + it.copy( + chartState = it.chartState.copy( + status = MarketsTokenDetailsUM.ChartState.Status.DATA, + ), + ) + } + }.onLeft { + state.update { + it.copy( + chartState = it.chartState.copy( + status = MarketsTokenDetailsUM.ChartState.Status.ERROR, + ), + ) + } + } + }.saveIn(loadChartJobHolder) + } + + private fun getFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { + return when (interval) { + PriceChangeInterval.H24 -> { value: BigDecimal -> + value.toLong().toTimeFormat(DateTimeFormatters.timeFormatter) + } + PriceChangeInterval.WEEK, + PriceChangeInterval.MONTH, + PriceChangeInterval.MONTH3, + PriceChangeInterval.MONTH6, + -> { value -> + value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd) + } + PriceChangeInterval.YEAR -> { value -> + value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd) + } + PriceChangeInterval.ALL_TIME -> { value -> + value.toLong().toTimeFormat(DateTimeFormatters.dateYYYY) } } } + + @Suppress("MagicNumber") + private fun onMarkerPointSelected(time: BigDecimal?, price: BigDecimal?) { + val timeText = time?.toLong()?.toTimeFormat(DateTimeFormatters.dateTimeFormatter)?.let { + resourceReference(R.string.common_range, wrappedList(it, resourceReference(R.string.common_now))) + } ?: resourceReference(R.string.common_today) + + val percent = price?.subtract(params.token.tokenQuotes.currentPrice) + ?.divide(params.token.tokenQuotes.currentPrice, 4, RoundingMode.HALF_UP) + ?.multiply(BigDecimal(-100)) + ?: params.token.tokenQuotes.h24Percent + + val percentText = BigDecimalFormatter.formatPercent( + percent = percent, + useAbsoluteValue = true, + ) + + state.update { + it.copy( + dateTimeText = timeText, + priceText = BigDecimalFormatter.formatFiatAmountUncapped( + fiatAmount = price ?: params.token.tokenQuotes.currentPrice, + fiatCurrencyCode = currentAppCurrency.value.code, + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ), + priceChangePercentText = percentText, + priceChangeType = when { + percent < BigDecimal.ZERO -> PriceChangeType.DOWN + percent > BigDecimal.ZERO -> PriceChangeType.UP + else -> PriceChangeType.NEUTRAL + }, + ) + } + + chartDataProducer.runTransaction { + updateLook { + it.copy( + type = getChartTypeByPercent(percent), + ) + } + } + } + + private fun getChartTypeByPercent(percent: BigDecimal): MarketChartLook.Type { + return if (percent >= BigDecimal.ZERO) { + MarketChartLook.Type.Growing + } else { + MarketChartLook.Type.Falling + } + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index 9f091059d5..ead8e056f4 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -1,31 +1,223 @@ package com.tangem.features.markets.details.impl.ui -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import com.tangem.common.ui.charts.state.MarketChartDataProducer +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH32 +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.components.SpacerW4 import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.components.currency.icon.CoinIcon +import com.tangem.core.ui.components.marketprice.PriceChangeInPercent +import com.tangem.core.ui.components.marketprice.PriceChangeType +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.stringReference +import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.markets.details.impl.ui.components.MarketTokenDetailsChart import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.persistentListOf @Suppress("UnusedPrivateMember") @Composable internal fun MarketsTokenDetailsContent( state: MarketsTokenDetailsUM, onBackClick: () -> Unit, + onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier, ) { - // TODO + Content( + state = state, + onBackClick = onBackClick, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) } @Suppress("UnusedPrivateMember") @Composable -private fun Content(state: MarketsTokenDetailsUM, onBackClick: () -> Unit, modifier: Modifier = Modifier) { - Column { +private fun Content( + state: MarketsTokenDetailsUM, + onBackClick: () -> Unit, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier = Modifier, +) { + val backgroundColor = LocalMainBottomSheetColor.current.value + val density = LocalDensity.current + + Column( + modifier = modifier + .drawBehind { drawRect(backgroundColor) } + .fillMaxSize(), + ) { TangemTopAppBar( + modifier = Modifier.onGloballyPositioned { + if (it.size.height > 0) { + with(density) { + onHeaderSizeChange(it.size.height.toDp()) + } + } + }, title = state.tokenName, startButton = TopAppBarButtonUM.Back(onBackClick), ) - } + SpacerH4() - // TODO + Header( + state = state, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) + + SpacerH16() + + IntervalSelector( + trendInterval = state.selectedInterval, + onIntervalClick = state.onSelectedIntervalChange, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) + + SpacerH32() + + MarketTokenDetailsChart( + modifier = Modifier.fillMaxWidth(), + state = state.chartState, + ) + } +} + +@Composable +private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column { + Text( + text = state.priceText, + style = TangemTheme.typography.head, + color = TangemTheme.colors.text.primary1, + ) + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { + Text( + text = state.dateTimeText.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + PriceChangeInPercent( + valueInPercent = state.priceChangePercentText, + type = state.priceChangeType, + textStyle = TangemTheme.typography.caption2, + ) + } + } + SpacerW4() + CoinIcon( + modifier = Modifier.size(TangemTheme.dimens.size48), + url = state.iconUrl, + alpha = 1f, + colorFilter = null, + fallbackResId = R.drawable.ic_custom_token_44, + ) + } +} + +@Composable +private fun IntervalSelector( + trendInterval: PriceChangeInterval, + onIntervalClick: (PriceChangeInterval) -> Unit, + modifier: Modifier = Modifier, +) { + SegmentedButtons( + config = persistentListOf( + PriceChangeInterval.H24, + PriceChangeInterval.WEEK, + PriceChangeInterval.MONTH, + PriceChangeInterval.MONTH3, + PriceChangeInterval.MONTH6, + PriceChangeInterval.YEAR, + PriceChangeInterval.ALL_TIME, + ), + color = TangemTheme.colors.button.secondary, + initialSelectedItem = trendInterval, + onClick = onIntervalClick, + modifier = modifier, + ) { + Box( + Modifier + .fillMaxSize() + .align(Alignment.Center) + .padding( + vertical = TangemTheme.dimens.spacing4, + ), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = it.getText().resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +@Composable +fun PriceChangeInterval.getText(): TextReference { + return when (this) { + PriceChangeInterval.H24 -> resourceReference(R.string.markets_selector_interval_24h_title) + PriceChangeInterval.WEEK -> resourceReference(R.string.markets_selector_interval_7d_title) + PriceChangeInterval.MONTH -> resourceReference(R.string.markets_selector_interval_1m_title) + PriceChangeInterval.MONTH3 -> resourceReference(R.string.markets_selector_interval_3m_title) + PriceChangeInterval.MONTH6 -> resourceReference(R.string.markets_selector_interval_6m_title) + PriceChangeInterval.YEAR -> resourceReference(R.string.markets_selector_interval_1y_title) + PriceChangeInterval.ALL_TIME -> resourceReference(R.string.markets_selector_interval_all_title) + } +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + Content( + state = MarketsTokenDetailsUM( + tokenName = "Token Name", + priceText = "Price", + dateTimeText = stringReference("Date Time"), + priceChangePercentText = "Price Change", + iconUrl = "", + priceChangeType = PriceChangeType.UP, + chartState = MarketsTokenDetailsUM.ChartState( + dataProducer = MarketChartDataProducer.build { }, + chartLook = MarketChartLook(), + onLoadRetryClick = {}, + status = MarketsTokenDetailsUM.ChartState.Status.LOADING, + onMarkerPointSelected = { _, _ -> }, + ), + selectedInterval = PriceChangeInterval.H24, + onSelectedIntervalChange = { }, + ), + onHeaderSizeChange = {}, + onBackClick = {}, + ) + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt index 8a92bdd1f3..d138a1c23d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt @@ -1,29 +1,76 @@ package com.tangem.features.markets.details.impl.ui.components +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import com.tangem.common.ui.charts.MarketChart +import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.rememberMarketChartState +import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData @Composable fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) { - val chartState = rememberMarketChartState(state.dataProducer) + val growingColor = TangemTheme.colors.icon.accent + val fallingColor = TangemTheme.colors.icon.warning - MarketChart( - modifier = modifier, - state = chartState, - noChartContent = { - UnableToLoadData( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12) - .align(Alignment.Center), - onRetryClick = state.onLoadRetryClick, - ) + val chartState = rememberMarketChartState( + dataProducer = state.dataProducer, + colorMapper = { + when (it) { + MarketChartLook.Type.Growing -> growingColor + MarketChartLook.Type.Falling -> fallingColor + } }, + onMarkerShown = state.onMarkerPointSelected, ) + + val backgroundColor = LocalMainBottomSheetColor.current.value + + Box(modifier) { + MarketChart( + modifier = Modifier.fillMaxWidth(), + state = chartState, + ) + + if (state.status != MarketsTokenDetailsUM.ChartState.Status.DATA) { + Box( + Modifier + .drawBehind { drawRect(backgroundColor) } + .matchParentSize(), + ) { + when (state.status) { + MarketsTokenDetailsUM.ChartState.Status.LOADING -> { + CircularProgressIndicator( + modifier = Modifier + .size(TangemTheme.dimens.size16) + .align(Alignment.Center), + color = TangemTheme.colors.text.accent, + strokeWidth = TangemTheme.dimens.size2, + ) + } + MarketsTokenDetailsUM.ChartState.Status.ERROR -> { + UnableToLoadData( + modifier = Modifier + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ) + .align(Alignment.Center), + onRetryClick = state.onLoadRetryClick, + ) + } + else -> {} + } + } + } + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt index 2599cd5f21..48941c6461 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt @@ -3,18 +3,31 @@ package com.tangem.features.markets.details.impl.ui.entity import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.markets.PriceChangeInterval +import java.math.BigDecimal data class MarketsTokenDetailsUM( val tokenName: String, - val dateTimeText: String, + val priceText: String, + val iconUrl: String, + val dateTimeText: TextReference, val priceChangePercentText: String, val priceChangeType: PriceChangeType, + val selectedInterval: PriceChangeInterval, val chartState: ChartState, + val onSelectedIntervalChange: (PriceChangeInterval) -> Unit, ) { data class ChartState( + val status: Status, val dataProducer: MarketChartDataProducer, val chartLook: MarketChartLook, val onLoadRetryClick: () -> Unit, - ) + val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit, + ) { + enum class Status { + LOADING, ERROR, DATA + } + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt index 849eaf46fd..6843a4d610 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/api/MarketsTokenListComponent.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.component.BottomSheetState @@ -20,6 +21,9 @@ interface MarketsTokenListComponent { ) interface Factory { - fun create(context: AppComponentContext, onTokenSelected: (TokenMarket) -> Unit): MarketsTokenListComponent + fun create( + context: AppComponentContext, + onTokenSelected: (TokenMarket, AppCurrency) -> Unit, + ): MarketsTokenListComponent } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt index 783559bd3c..5a73f13956 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.unit.Dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.component.BottomSheetState import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent @@ -22,14 +23,14 @@ import kotlinx.coroutines.flow.onEach class DefaultMarketsTokenListComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted private val onTokenSelected: (TokenMarket) -> Unit, + @Assisted private val onTokenSelected: (TokenMarket, AppCurrency) -> Unit, ) : AppComponentContext by appComponentContext, MarketsTokenListComponent { private val model: MarketsListModel = getOrCreateModel() init { model.tokenSelected - .onEach { onTokenSelected(it) } + .onEach { onTokenSelected(it.first, it.second) } .launchIn(componentScope) } @@ -58,7 +59,7 @@ class DefaultMarketsTokenListComponent @AssistedInject constructor( interface Factory : MarketsTokenListComponent.Factory { override fun create( context: AppComponentContext, - onTokenSelected: (TokenMarket) -> Unit, + onTokenSelected: (TokenMarket, AppCurrency) -> Unit, ): DefaultMarketsTokenListComponent } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index 06fe406bca..b2aca92901 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -77,7 +77,7 @@ internal class MarketsListModel @Inject constructor( private var activeListManager: MarketsListBatchFlowManager = mainMarketsListManager - private val _tokenSelected = MutableSharedFlow() + private val _tokenSelected = MutableSharedFlow>() val tokenSelected = _tokenSelected.asSharedFlow() @@ -223,7 +223,7 @@ internal class MarketsListModel @Inject constructor( private fun onTokenUIClicked(token: MarketsListItemUM) { modelScope.launch { activeListManager.getTokenById(token.id)?.let { found -> - _tokenSelected.emit(found) + _tokenSelected.emit(found to currentAppCurrency.value) } } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index 17050e129d..f27ba0a768 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -1,6 +1,6 @@ package com.tangem.features.markets.tokenlist.impl.model.converters -import com.tangem.common.ui.charts.state.DefaultPointValuesConverter +import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType @@ -10,6 +10,7 @@ import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM.TrendInterval import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal import java.math.RoundingMode @@ -18,6 +19,8 @@ internal class MarketsTokenItemConverter( private val appCurrency: AppCurrency, ) : Converter { + private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = false) + override fun convert(value: TokenMarket): MarketsListItemUM { return MarketsListItemUM( id = value.id, @@ -107,10 +110,10 @@ internal class MarketsTokenItemConverter( } return chart?.let { ct -> - DefaultPointValuesConverter.convert( + priceAndTimePointValuesConverter.convert( MarketChartData.Data( - y = ct.priceY, - x = ct.timeStamps.map { it.toBigDecimal() }, + y = ct.priceY.toImmutableList(), + x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(), ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt index 42a790727c..ba4b9aba33 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt @@ -233,7 +233,7 @@ internal class MarketsListBatchFlowManager( BatchAction.UpdateBatches( keys = batchesKeysToLoad, updateRequest = TokenMarketUpdateRequest.UpdateChart( - interval = interval.toRequestInterval(), + interval = interval.toBatchRequestInterval(), currency = currentAppCurrency().code, ), async = true, @@ -303,14 +303,6 @@ internal class MarketsListBatchFlowManager( } } - private fun TrendInterval.toRequestInterval(): PriceChangeInterval { - return when (this) { - TrendInterval.H24 -> PriceChangeInterval.H24 - TrendInterval.D7 -> PriceChangeInterval.WEEK - TrendInterval.M1 -> PriceChangeInterval.MONTH - } - } - private fun Flow.onEachWithPrevious(operation: suspend (prev: T?, value: T) -> Unit): Flow = flow { var prev: T? = null collect { value -> diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 72f70d8971..56f0361cff 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -1,16 +1,15 @@ package com.tangem.features.markets.tokenlist.impl.ui +import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager @@ -32,6 +31,7 @@ import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.component.BottomSheetState @@ -68,23 +68,28 @@ internal fun MarketsList( @Composable private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) { val density = LocalDensity.current + val background = LocalMainBottomSheetColor.current.value Column( modifier = modifier .fillMaxSize() .imePadding() - .background(color = TangemTheme.colors.background.primary), + .drawBehind { drawRect(background) }, ) { SearchBar( modifier = Modifier - .background(color = TangemTheme.colors.background.primary) + .drawBehind { drawRect(background) } .padding( start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16, bottom = TangemTheme.dimens.spacing4, ) .onGloballyPositioned { - with(density) { onHeaderSizeChange(it.size.height.toDp()) } + if (it.size.height > 0) { + with(density) { + onHeaderSizeChange(it.size.height.toDp()) + } + } }, state = state.searchBar, ) @@ -225,45 +230,52 @@ private fun KeyboardEvents(isSortByBottomSheetShown: Boolean, bottomSheetState: //region: Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun Preview() { - TangemThemePreview { - MarketsList( - state = MarketsListUM( - list = ListUM.Content( - items = MarketChartListItemPreviewDataProvider().values - .flatMap { item -> List(size = 10) { item } } - .mapIndexed { index, item -> - item.copy(id = index.toString()) - } - .toImmutableList(), - showUnder100kTokens = false, - loadMore = {}, - visibleIdsChanged = {}, - onShowTokensUnder100kClicked = {}, - triggerScrollReset = consumedEvent(), - onItemClick = {}, + TangemThemePreview(alwaysShowBottomSheets = false) { + val primaryBackground = TangemTheme.colors.background.primary + + CompositionLocalProvider( + LocalMainBottomSheetColor provides remember { mutableStateOf(primaryBackground) }, + ) { + MarketsList( + state = MarketsListUM( + list = ListUM.Content( + items = MarketChartListItemPreviewDataProvider().values + .flatMap { item -> List(size = 10) { item } } + .mapIndexed { index, item -> + item.copy(id = index.toString()) + } + .toImmutableList(), + showUnder100kTokens = false, + loadMore = {}, + visibleIdsChanged = {}, + onShowTokensUnder100kClicked = {}, + triggerScrollReset = consumedEvent(), + onItemClick = {}, + ), + searchBar = SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = { }, + ), + selectedSortBy = SortByTypeUM.Rating, + selectedInterval = MarketsListUM.TrendInterval.H24, + onIntervalClick = {}, + onSortByButtonClick = {}, + sortByBottomSheet = TangemBottomSheetConfig( + isShow = false, + onDismissRequest = {}, + content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, + ), ), - searchBar = SearchBarUM( - placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), - query = "", - onQueryChange = {}, - isActive = false, - onActiveChange = { }, - ), - selectedSortBy = SortByTypeUM.Rating, - selectedInterval = MarketsListUM.TrendInterval.H24, - onIntervalClick = {}, - onSortByButtonClick = {}, - sortByBottomSheet = TangemBottomSheetConfig( - isShow = false, - onDismissRequest = {}, - content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, - ), - ), - onHeaderSizeChange = {}, - bottomSheetState = BottomSheetState.EXPANDED, - ) + onHeaderSizeChange = {}, + bottomSheetState = BottomSheetState.EXPANDED, + ) + } } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index 56c53b9a72..8745966639 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -89,7 +89,7 @@ internal fun MarketsListLazyColumn( is ListUM.Content -> { items( items = state.items, - key = { it.id }, + key = { it.id + it.marketCap.toString() }, ) { item -> MarketsListItem( model = item, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt index fd5efe0400..5cc524cd87 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM +import kotlinx.collections.immutable.persistentListOf internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( collection = listOf( @@ -19,7 +20,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet trendPercentText = "12.43%", trendType = PriceChangeType.UP, chardData = MarketChartRawData( - y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), ), MarketsListItemUM( @@ -45,7 +46,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet trendPercentText = "12.43%", trendType = PriceChangeType.DOWN, chardData = MarketChartRawData( - y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), ), MarketsListItemUM( @@ -59,7 +60,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet trendPercentText = "12.43%", trendType = PriceChangeType.UP, chardData = MarketChartRawData( - y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), ), MarketsListItemUM( @@ -73,7 +74,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet trendPercentText = "12.43%", trendType = PriceChangeType.UP, chardData = MarketChartRawData( - y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), ), MarketsListItemUM( @@ -87,7 +88,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet trendPercentText = "12.43%", trendType = PriceChangeType.UP, chardData = MarketChartRawData( - y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0), ), ), ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index d671a0c9c5..868de285aa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation import android.os.Bundle import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.arkivanov.decompose.defaultComponentContext import com.tangem.core.decompose.context.DefaultAppComponentContext @@ -68,7 +69,11 @@ internal class WalletFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { _walletRouter.Initialize( - onFinish = requireActivity()::finish, + onFinish = remember(requireActivity()) { + { + requireActivity().finish() + } + }, marketsEntryComponent = marketsEntryComponent, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index 9285575d9e..160e0d11da 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -1,8 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model +import androidx.compose.runtime.Immutable import com.tangem.core.ui.event.StateEvent import kotlinx.collections.immutable.ImmutableList +@Immutable internal data class WalletScreenState( val onBackClick: () -> Unit, val topBarConfig: WalletTopBarConfig, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index a6bb2a02db..b8dd2da1b7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -49,6 +49,8 @@ import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TestTags @@ -70,6 +72,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balances import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator import com.tangem.features.markets.component.BottomSheetState +import com.tangem.features.markets.component.BottomSheetState.* import com.tangem.features.markets.component.MarketsEntryComponent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch @@ -316,15 +319,15 @@ private fun BaseScaffold( @Suppress("LongParameterList", "LongMethod") @OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable -private fun BaseScaffoldWithMarkets( +private inline fun BaseScaffoldWithMarkets( state: WalletScreenState, selectedWallet: WalletState, snackbarHostState: SnackbarHostState, bottomSheetHeaderHeightProvider: () -> Dp, - bottomSheetContent: @Composable () -> Unit, + crossinline bottomSheetContent: @Composable () -> Unit, alertConfig: WalletAlertState?, - onBottomSheetStateChange: (BottomSheetState) -> Unit, - content: @Composable () -> Unit, + noinline onBottomSheetStateChange: (BottomSheetState) -> Unit, + crossinline content: @Composable () -> Unit, ) { // show the bottom sheet if there is at least one multicurrency wallet val showManageTokensBottomSheet = remember(state.wallets) { @@ -332,11 +335,13 @@ private fun BaseScaffoldWithMarkets( } val bottomSheetState = rememberSheetStateEnhanced( initialValue = if (showManageTokensBottomSheet) SheetValue.PartiallyExpanded else SheetValue.Hidden, - confirmValueChange = { sheetValue -> - when { - sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false - sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false - else -> true + confirmValueChange = remember(showManageTokensBottomSheet) { + { sheetValue -> + when { + sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false + sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false + else -> true + } } }, skipHiddenState = showManageTokensBottomSheet, @@ -344,14 +349,6 @@ private fun BaseScaffoldWithMarkets( val keyboardShown = keyboardAsState() - BottomSheetStateEffects( - bottomSheetState = bottomSheetState, - showManageTokensBottomSheet = showManageTokensBottomSheet, - alertConfig = alertConfig, - keyboardShown = keyboardShown, - onBottomSheetStateChange = onBottomSheetStateChange, - ) - val scaffoldState = rememberBottomSheetScaffoldState( bottomSheetState = bottomSheetState, snackbarHostState = snackbarHostState, @@ -360,77 +357,89 @@ private fun BaseScaffoldWithMarkets( val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight + val maxHeight = LocalWindowSize.current.height val coroutineScope = rememberCoroutineScope() + val backgroundPrimary = TangemTheme.colors.background.primary - BottomSheetScaffold( - snackbarHost = { - WalletSnackbarHost( - snackbarHostState = it, - event = state.event, - modifier = Modifier - .padding(bottom = TangemTheme.dimens.spacing4) - .navigationBarsPadding(), - ) - }, - containerColor = TangemTheme.colors.background.secondary, - sheetContainerColor = TangemTheme.colors.background.primary, - scaffoldState = scaffoldState, - sheetPeekHeight = peekHeight, - sheetDragHandle = { - Hand(modifier = Modifier.background(color = TangemTheme.colors.background.primary)) - }, - sheetTonalElevation = 8.dp, - sheetShadowElevation = 8.dp, - sheetContent = { - BoxWithConstraints { - Box( + CompositionLocalProvider( + LocalMainBottomSheetColor provides remember { mutableStateOf(backgroundPrimary) }, + ) { + val backgroundColor = LocalMainBottomSheetColor.current + + BottomSheetStateEffects( + bottomSheetState = bottomSheetState, + showManageTokensBottomSheet = showManageTokensBottomSheet, + alertConfig = alertConfig, + keyboardShown = keyboardShown, + onBottomSheetStateChange = onBottomSheetStateChange, + ) + + BottomSheetScaffold( + snackbarHost = { + WalletSnackbarHost( + snackbarHostState = it, + event = state.event, modifier = Modifier - .sizeIn(maxHeight = maxHeight - statusBarHeight) - .align(Alignment.BottomCenter), + .padding(bottom = TangemTheme.dimens.spacing4) + .navigationBarsPadding(), + ) + }, + containerColor = TangemTheme.colors.background.secondary, + sheetContainerColor = backgroundColor.value, + scaffoldState = scaffoldState, + sheetPeekHeight = peekHeight, + sheetDragHandle = { + Hand(modifier = Modifier.background(color = backgroundColor.value)) + }, + sheetTonalElevation = 8.dp, + sheetShadowElevation = 8.dp, + sheetContent = { + Box( + modifier = Modifier.sizeIn(maxHeight = maxHeight - statusBarHeight - handComposableComponentHeight), ) { bottomSheetContent() } - } - // hide bottom sheet when back pressed - BackHandler( - keyboardShown.value is Keyboard.Closed && - bottomSheetState.currentValue == SheetValue.Expanded, - ) { - coroutineScope.launch { bottomSheetState.partialExpand() } - } - }, - content = { _ -> - val pullRefreshState = rememberPullRefreshState( - refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - onRefresh = { - selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) - }, - ) - - Column { - WalletTopBar(config = state.topBarConfig) - Box( - modifier = Modifier.pullRefresh(pullRefreshState), + // hide bottom sheet when back pressed + BackHandler( + keyboardShown.value is Keyboard.Closed && + bottomSheetState.currentValue == SheetValue.Expanded, ) { - content() - - WalletPullToRefreshIndicator( - isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - ) + coroutineScope.launch { bottomSheetState.partialExpand() } } - } + }, + content = { _ -> + val pullRefreshState = rememberPullRefreshState( + refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + onRefresh = { + selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) + }, + ) - BottomSheetScrim( - color = BottomSheetDefaults.ScrimColor, - visible = bottomSheetState.targetValue == SheetValue.Expanded, - onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } }, - ) - }, - ) + Column { + WalletTopBar(config = state.topBarConfig) + Box( + modifier = Modifier.pullRefresh(pullRefreshState), + ) { + content() + + WalletPullToRefreshIndicator( + isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) + } + } + + BottomSheetScrim( + color = BottomSheetDefaults.ScrimColor, + visible = bottomSheetState.targetValue == SheetValue.Expanded, + onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } }, + ) + }, + ) + } } @Composable @@ -545,9 +554,9 @@ private fun BottomSheetStateEffects( LaunchedEffect(isSheetHidden) { onBottomSheetStateChange( if (isSheetHidden) { - BottomSheetState.COLLAPSED + COLLAPSED } else { - BottomSheetState.EXPANDED + EXPANDED }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 3a9681b918..f4ac9a87e3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels +import androidx.compose.runtime.Stable import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -44,6 +45,7 @@ import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") +@Stable @HiltViewModel internal class WalletViewModel @Inject constructor( private val stateHolder: WalletStateController, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 1c1542e3b1..5dd3fc5ba9 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -92,7 +92,7 @@ tangemBlockchainSdk = "develop-713" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ -tangemVico = "2.0.0-alpha.21-tangem14" +tangemVico = "2.0.0-alpha.25-tangem16" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem From dd26ac3c0f4841af02a29620820f54dd659f1ff2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Aug 2024 14:37:20 +0500 Subject: [PATCH 16/87] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 8 +++++++- .../configs/feature_toggles_config.json | 4 ++++ .../managetokens/ManageTokensToggles.kt | 5 +++++ features/manage-tokens/impl/build.gradle.kts | 1 + .../DefaultManageTokensToggles.kt | 11 ++++++++++ .../features/managetokens/di/FeatureModule.kt | 20 +++++++++++++++++++ 6 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/ManageTokensToggles.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/DefaultManageTokensToggles.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/FeatureModule.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 8b3c22a7f9..082fe2c2a0 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -9,6 +9,7 @@ import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent +import com.tangem.features.managetokens.ManageTokensToggles import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter @@ -55,6 +56,7 @@ internal class ChildFactory @Inject constructor( private val testerRouter: TesterRouter, private val detailsFeatureToggles: DetailsFeatureToggles, private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles, + private val manageTokensToggles: ManageTokensToggles, private val pushNotificationRouter: PushNotificationsRouter, ) { @@ -119,7 +121,11 @@ internal class ChildFactory @Inject constructor( route.asFragmentChild(Provider { HomeFragment() }) } is AppRoute.ManageTokens -> { - route.asFragmentChild(Provider { TokensListFragment() }) + if (manageTokensToggles.isFeatureEnabled) { + TODO("Not implemented yet") // [REDACTED_JIRA] + } else { + route.asFragmentChild(Provider { TokensListFragment() }) + } } is AppRoute.OnboardingNote -> { route.asFragmentChild(Provider { OnboardingNoteFragment() }) 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 7a5cc57245..bde5433abf 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -42,5 +42,9 @@ { "name": "HOME_SCREEN_CALLBACKS_REFACTORING_ENABLED", "version": "5.14.0" + }, + { + "name": "NEW_MANAGE_TOKENS", + "version": "undefined" } ] diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/ManageTokensToggles.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/ManageTokensToggles.kt new file mode 100644 index 0000000000..09ad4a0dcd --- /dev/null +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/ManageTokensToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.managetokens + +interface ManageTokensToggles { + val isFeatureEnabled: Boolean +} \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 8e3cc2bddb..05647d3743 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.ui) implementation(projects.common.routing) + implementation(projects.core.featuretoggles) /* AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/DefaultManageTokensToggles.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/DefaultManageTokensToggles.kt new file mode 100644 index 0000000000..ea923c7772 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/DefaultManageTokensToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.managetokens + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultManageTokensToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : ManageTokensToggles { + + override val isFeatureEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("NEW_MANAGE_TOKENS") +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/FeatureModule.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/FeatureModule.kt new file mode 100644 index 0000000000..9c181b867e --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/FeatureModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.managetokens.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.managetokens.DefaultManageTokensToggles +import com.tangem.features.managetokens.ManageTokensToggles +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 FeatureModule { + + @Provides + @Singleton + fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): ManageTokensToggles = + DefaultManageTokensToggles(featureTogglesManager = featureTogglesManager) +} \ No newline at end of file From 65ead13295c77eb8989d7c691549d42c545d05aa Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Aug 2024 17:57:18 +0500 Subject: [PATCH 17/87] Updated on 2026-08-14 --- .../features/home/compose/StoriesScreen.kt | 2 +- .../tangem/tap/routing/utils/ChildFactory.kt | 8 +- .../component/ManageTokensComponent.kt | 5 +- .../impl/DefaultManageTokensComponent.kt | 40 ++++ .../managetokens/di/ComponentModule.kt | 18 ++ .../features/managetokens/di/ModelModule.kt | 20 ++ .../managetokens/model/ManageTokensModel.kt | 192 ++++++++++++++++++ 7 files changed, 279 insertions(+), 6 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ModelModule.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt 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 1b0c69fb9c..9f4e9fbaa9 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 @@ -33,7 +33,7 @@ import com.tangem.wallet.R import kotlin.math.max @Composable -fun StoriesScreen( +internal fun StoriesScreen( homeState: MutableState, onScanButtonClick: () -> Unit, onShopButtonClick: () -> Unit, diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 082fe2c2a0..b6d85b29ba 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -10,6 +10,7 @@ import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.managetokens.ManageTokensToggles +import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter @@ -48,6 +49,7 @@ internal class ChildFactory @Inject constructor( private val detailsComponentFactory: DetailsComponent.Factory, private val walletSettingsComponentFactory: WalletSettingsComponent.Factory, private val disclaimerComponentFactory: DisclaimerComponent.Factory, + private val manageTokensComponentFactory: ManageTokensComponent.Factory, private val sendRouter: SendRouter, private val tokenDetailsRouter: TokenDetailsRouter, private val walletRouter: WalletRouter, @@ -122,7 +124,11 @@ internal class ChildFactory @Inject constructor( } is AppRoute.ManageTokens -> { if (manageTokensToggles.isFeatureEnabled) { - TODO("Not implemented yet") // [REDACTED_JIRA] + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = ManageTokensComponent.Params(), + componentFactory = manageTokensComponentFactory, + ) } else { route.asFragmentChild(Provider { TokensListFragment() }) } diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt index 54cb358614..921c452548 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt @@ -2,13 +2,10 @@ package com.tangem.features.managetokens.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.wallets.models.UserWalletId interface ManageTokensComponent : ComposableContentComponent { - data class Params( - val userWalletId: UserWalletId, - ) + class Params interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt new file mode 100644 index 0000000000..e89139cf5f --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.managetokens.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.model.ManageTokensModel +import com.tangem.features.managetokens.ui.ManageTokensScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultManageTokensComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: ManageTokensComponent.Params, +) : ManageTokensComponent, AppComponentContext by context { + + private val model: ManageTokensModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + ManageTokensScreen( + modifier = modifier, + state = state, + ) + } + + @AssistedFactory + interface Factory : ManageTokensComponent.Factory { + override fun create( + context: AppComponentContext, + params: ManageTokensComponent.Params, + ): DefaultManageTokensComponent + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt new file mode 100644 index 0000000000..f84b3c4c8e --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.managetokens.di + +import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.component.impl.DefaultManageTokensComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindManageTokensComponentFactory(factory: DefaultManageTokensComponent.Factory): ManageTokensComponent.Factory +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ModelModule.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ModelModule.kt new file mode 100644 index 0000000000..572c2624cb --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.managetokens.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.managetokens.model.ManageTokensModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(ManageTokensModel::class) + fun provideManageTokensModel(model: ManageTokensModel): Model +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt new file mode 100644 index 0000000000..b7527834c1 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -0,0 +1,192 @@ +package com.tangem.features.managetokens.model + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.components.rows.model.ChainRowUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.CurrencyNetworkUM +import com.tangem.features.managetokens.entity.ManageTokensUM +import com.tangem.features.managetokens.impl.R +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@ComponentScoped +internal class ManageTokensModel @Inject constructor( + private val router: Router, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val changedItemsIds: MutableSet = mutableSetOf() + private var items = initItems() + + val state: MutableStateFlow = MutableStateFlow( + value = ManageTokensUM( + popBack = router::pop, + items = items, + search = SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "", + onQueryChange = ::searchCurrencies, + isActive = false, + onActiveChange = ::toggleSearchBar, + ), + hasChanges = false, + onSaveClick = {}, + onAddCustomToken = {}, + ), + ) + + @Suppress("UnusedPrivateMember") + private fun searchCurrencies(query: String) { + // TODO: [REDACTED_JIRA] + } + + private fun toggleSearchBar(isActive: Boolean) { + state.update { state -> + state.copy( + search = state.search.copy(isActive = isActive), + ) + } + } + + private fun initItems() = List(size = 30) { index -> + if (index < 2) { + getCustomItem(index) + } else { + getBasicItem(index) + } + }.toPersistentList() + + private fun getCustomItem(index: Int) = CurrencyItemUM.Custom( + id = index.toString(), + model = ChainRowUM( + name = "Custom token $index", + type = "CT$index", + icon = CurrencyIconState.CustomTokenIcon( + tint = Color.White, + background = Color.Black, + topBadgeIconResId = R.drawable.img_eth_22, + isGrayscale = false, + showCustomBadge = true, + ), + showCustom = true, + ), + onRemoveClick = {}, + ) + + private fun getBasicItem(index: Int) = CurrencyItemUM.Basic( + id = index.toString(), + model = ChainRowUM( + name = "Currency $index", + type = "C$index", + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_btc_22, + isGrayscale = false, + showCustomBadge = false, + ), + showCustom = false, + ), + networks = if (index == 2) { + CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) + } else { + CurrencyItemUM.Basic.NetworksUM.Collapsed + }, + onExpandClick = { toggleCurrency(index) }, + ) + + private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex -> + CurrencyNetworkUM( + id = networkIndex.toString(), + model = BlockchainRowUM( + name = "NETWORK$networkIndex", + type = "N$networkIndex", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = networkIndex == 0, + isSelected = false, + ), + isSelected = false, + onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) }, + ) + }.toImmutableList() + + private fun toggleCurrency(index: Int) { + val updatedItem = when (val item = items[index]) { + is CurrencyItemUM.Basic -> item.copy( + networks = if (item.networks is CurrencyItemUM.Basic.NetworksUM.Collapsed) { + CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) + } else { + CurrencyItemUM.Basic.NetworksUM.Collapsed + }, + ) + is CurrencyItemUM.Custom -> return + } + + state.update { state -> + items = items.mutate { + it[index] = updatedItem + } + state.copy(items = items) + } + } + + private fun toggleNetwork(currencyIndex: Int, networkIndex: Int, isSelected: Boolean) { + val updatedItem = when (val item = items[currencyIndex]) { + is CurrencyItemUM.Basic -> { + val updatedNetworks = (item.networks as? CurrencyItemUM.Basic.NetworksUM.Expanded) + ?.copy( + networks = item.networks.networks.toPersistentList().mutate { + it.fastForEachIndexed { index, network -> + if (index == networkIndex) { + it[index] = network.copy( + model = network.model.copy( + iconResId = if (isSelected) { + R.drawable.img_eth_22 + } else { + R.drawable.ic_eth_16 + }, + isSelected = isSelected, + ), + isSelected = isSelected, + ) + } + } + }, + ) + ?: return + + item.copy(networks = updatedNetworks) + } + is CurrencyItemUM.Custom -> return + } + + val id = "${currencyIndex}_$networkIndex" + if (changedItemsIds.contains(id)) { + changedItemsIds.remove(id) + } else { + changedItemsIds.add(id) + } + + state.update { state -> + items = items.mutate { + it[currencyIndex] = updatedItem + } + state.copy( + items = items, + hasChanges = changedItemsIds.isNotEmpty(), + ) + } + } +} \ No newline at end of file From 6227d835b0cec7fcc8fe9316f065b098820d5901 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Aug 2024 16:16:39 +0300 Subject: [PATCH 18/87] Updated on 2026-08-14 --- .../model/error/StakeKitErrorResponse.kt | 2 +- .../list/RoundedListWithDividers.kt | 35 +++- .../data/staking/DefaultStakingRepository.kt | 24 +-- .../impl/presentation/state/FeeState.kt | 2 + .../state/InnerYieldBalanceState.kt | 6 +- .../presentation/state/StakingNotification.kt | 11 +- .../state/StakingStateController.kt | 2 + .../impl/presentation/state/StakingUiState.kt | 1 + .../converters/YieldBalancesConverter.kt | 1 + .../previewdata/InitialStakingStatePreview.kt | 2 +- .../AddStakingErrorTransformer.kt | 36 ++++ .../SetInitialDataStateTransformer.kt | 157 ++++++++++++------ .../ShowInfoBottomSheetStateTransformer.kt | 4 +- .../ui/StakingInitialInfoContent.kt | 36 ++-- .../impl/presentation/ui/StakingScreen.kt | 3 +- .../ui/block/NotificationsBlock.kt | 16 +- .../viewmodel/StakingViewModel.kt | 5 +- 17 files changed, 250 insertions(+), 93 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt index 30396de002..9ad42062ca 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt @@ -16,7 +16,7 @@ class StakeKitErrorResponse( @Json(name = "code") val code: String? = null, @Json(name = "countryCode") - val countryCode: String, + val countryCode: String?, @Json(name = "regionCode") val regionCode: String? = null, @Json(name = "tags") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt index 1779adf487..336f35d454 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt @@ -16,16 +16,39 @@ 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.res.TangemThemePreview +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +private const val ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY = "ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY" +private const val ROUNDED_LIST_WITH_DIVIDERS_FOOTER_KEY = "ROUNDED_LIST_WITH_DIVIDERS_FOOTER_KEY" + @Composable -fun RoundedListWithDividers(rows: List, modifier: Modifier = Modifier) { +fun RoundedListWithDividers( + rows: ImmutableList, + modifier: Modifier = Modifier, + headerContent: (@Composable () -> Unit)? = null, + footerContent: (@Composable () -> Unit)? = null, +) { LazyColumn(modifier = modifier) { - this.roundedListItems(rows) + this.roundedListWithDividersItems( + rows = rows, + headerContent = headerContent, + footerContent = footerContent, + ) } } -fun LazyListScope.roundedListItems(rows: List) { +fun LazyListScope.roundedListWithDividersItems( + rows: ImmutableList, + headerContent: (@Composable () -> Unit)? = null, + footerContent: (@Composable () -> Unit)? = null, +) { + if (headerContent != null) { + item(key = ROUNDED_LIST_WITH_DIVIDERS_HEADER_KEY) { + headerContent() + } + } + itemsIndexed( items = rows, key = { _, item -> item.id }, @@ -40,6 +63,12 @@ fun LazyListScope.roundedListItems(rows: List) RoundedListDivider() } } + + if (footerContent != null) { + item(key = ROUNDED_LIST_WITH_DIVIDERS_FOOTER_KEY) { + footerContent() + } + } } @Composable diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index a8c7009e7b..6f5ce7909c 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -46,6 +46,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.orZero import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -135,7 +136,7 @@ internal class DefaultStakingRepository( val yield = getYield(cryptoCurrencyId, symbol) StakingEntryInfo( - interestRate = yield.apy, + interestRate = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr), periodInDays = yield.metadata.cooldownPeriod.days, tokenSymbol = yield.token.symbol, ) @@ -568,23 +569,24 @@ internal class DefaultStakingRepository( const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" const val TRON_INTEGRATION_ID = "tron-trx-native-staking" const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" - const val BINANCE_INTEGRATION_ID = "binance-bnb-native-staking" + const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking" const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" const val NEAR_INTEGRATION_ID = "near-near-native-staking" const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" + // uncomment items as implementation is ready val integrationIdMap = mapOf( Blockchain.Solana.toCoinId() to SOLANA_INTEGRATION_ID, Blockchain.Cosmos.toCoinId() to COSMOS_INTEGRATION_ID, - Blockchain.Polkadot.toCoinId() to POLKADOT_INTEGRATION_ID, - Blockchain.Polygon.toCoinId() to ETHEREUM_INTEGRATION_ID, - Blockchain.Avalanche.toCoinId() to AVALANCHE_INTEGRATION_ID, - Blockchain.Tron.toCoinId() to TRON_INTEGRATION_ID, - Blockchain.Cronos.toCoinId() to CRONOS_INTEGRATION_ID, - Blockchain.Binance.toCoinId() to BINANCE_INTEGRATION_ID, - Blockchain.Kava.toCoinId() to KAVA_INTEGRATION_ID, - Blockchain.Near.toCoinId() to NEAR_INTEGRATION_ID, - Blockchain.Tezos.toCoinId() to TEZOS_INTEGRATION_ID, + // Blockchain.Polkadot.toCoinId() to POLKADOT_INTEGRATION_ID, + // Blockchain.Polygon.toCoinId() to ETHEREUM_INTEGRATION_ID, + // Blockchain.Avalanche.toCoinId() to AVALANCHE_INTEGRATION_ID, + // Blockchain.Tron.toCoinId() to TRON_INTEGRATION_ID, + // Blockchain.Cronos.toCoinId() to CRONOS_INTEGRATION_ID, + // Blockchain.Binance.toCoinId() to BINANCE_INTEGRATION_ID, + // Blockchain.Kava.toCoinId() to KAVA_INTEGRATION_ID, + // Blockchain.Near.toCoinId() to NEAR_INTEGRATION_ID, + // Blockchain.Tezos.toCoinId() to TEZOS_INTEGRATION_ID, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt index c58332a870..ad93ecd3a3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt @@ -1,9 +1,11 @@ package com.tangem.features.staking.impl.presentation.state +import androidx.compose.runtime.Immutable import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency import java.math.BigDecimal +@Immutable sealed class FeeState { data class Content( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index 78ef0dc9dd..ce7d7eecb5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state +import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.PendingAction @@ -12,12 +13,14 @@ sealed class InnerYieldBalanceState { val rewardsCrypto: String, val rewardsFiat: String, val isRewardsToClaim: Boolean, - val balance: List, + val balance: ImmutableList, ) : InnerYieldBalanceState() data object Empty : InnerYieldBalanceState() } +// TODO staking get rid of unstable types +@Immutable data class BalanceGroupedState( val items: ImmutableList, val footer: TextReference?, @@ -26,6 +29,7 @@ data class BalanceGroupedState( val isClickable: Boolean, ) +@Immutable data class BalanceState( val validator: Yield.Validator, val cryptoValue: String, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt index 967f681edb..525eed8a61 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state 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.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.staking.impl.R @@ -23,7 +24,15 @@ internal sealed class StakingNotification(val config: NotificationConfig) { onCloseClick = onCloseClick, ), ) { - // TODO staking + data class StakedPositionNotFoundError(val message: String) : Error( + title = stringReference(message), + subtitle = stringReference(message), + ) + + data class Common(val subtitle: TextReference) : Error( + title = resourceReference(R.string.common_error), + subtitle = subtitle, + ) } sealed class Warning( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 2dd8aa4900..2556ac4e2a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer @@ -42,6 +43,7 @@ internal class StakingStateController @Inject constructor() { private fun getInitialState(): StakingUiState { return StakingUiState( + title = TextReference.EMPTY, clickIntents = StakingClickIntentsStub, cryptoCurrencyName = "", currentStep = StakingStep.InitialInfo, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 95f3d4cdab..98b09f5e78 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -18,6 +18,7 @@ import kotlinx.collections.immutable.ImmutableList */ @Immutable internal data class StakingUiState( + val title: TextReference, val clickIntents: StakingClickIntents, val cryptoCurrencyName: String, val currentStep: StakingStep, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 3411ac4946..85ad364a40 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -69,6 +69,7 @@ internal class YieldBalancesConverter( ) } } + .toPersistentList() private fun List.mapBalances(): List { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 2a5b57532b..f7d431316d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -65,7 +65,7 @@ internal object InitialStakingStatePreview { rewardsFiat = "100 $", rewardsCrypto = "100 SOL", isRewardsToClaim = false, - balance = listOf( + balance = persistentListOf( BalanceGroupedState( title = stringReference("Staked"), footer = null, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt new file mode 100644 index 0000000000..69278afeb6 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt @@ -0,0 +1,36 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class AddStakingErrorTransformer( + private val error: StakingError, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + val confirmationState = + prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState + + return prevState.copy( + confirmationState = confirmationState.copy( + notifications = (confirmationState.notifications + convertToNotification(error)).toPersistentList(), + feeState = FeeState.Error, + ), + ) + } + + private fun convertToNotification(error: StakingError): StakingNotification { + return when (error) { + is StakingError.StakedPositionNotFoundError -> StakingNotification.Error.StakedPositionNotFoundError( + message = error.toString(), + ) + // TODO staking + else -> StakingNotification.Error.Common( + subtitle = stringReference(error.toString()), + ) + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 6023ae90fb..1cecfa9476 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -5,10 +5,7 @@ import com.tangem.common.ui.amountScreen.converters.AmountStateConverter import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.list.RoundedListWithDividersItemData -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.Yield @@ -28,7 +25,7 @@ import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal internal class SetInitialDataStateTransformer( @@ -62,6 +59,10 @@ internal class SetInitialDataStateTransformer( override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( + title = TextReference.Res( + R.string.staking_initial_info_title, + wrappedList(cryptoCurrencyStatusProvider().currency.name), + ), clickIntents = clickIntents, currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), @@ -87,70 +88,118 @@ internal class SetInitialDataStateTransformer( val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val yieldBalance = cryptoCurrencyStatus.value.yieldBalance - return persistentListOf( - RoundedListWithDividersItemData( - id = R.string.staking_details_available, - startText = TextReference.Res(R.string.staking_details_available), - endText = TextReference.Str( - value = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoCurrencyStatus.value.amount, - cryptoCurrency = cryptoCurrencyStatus.currency.symbol, - decimals = cryptoCurrencyStatus.currency.decimals, - ), + return listOfNotNull( + createAvailableItem(cryptoCurrencyStatus), + createApyItem(), + createOnStakeItem(cryptoCurrencyStatus, yieldBalance), + createUnbondingPeriodItem(), + createMinimumRequirementItem(cryptoCurrencyStatus), + createRewardClaimingItem(), + createWarmupPeriodItem(), + createRewardScheduleItem(), + ).toPersistentList() + } + + private fun createAvailableItem(cryptoCurrencyStatus: CryptoCurrencyStatus): RoundedListWithDividersItemData { + return RoundedListWithDividersItemData( + id = R.string.staking_details_available, + startText = TextReference.Res(R.string.staking_details_available), + endText = TextReference.Str( + value = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoCurrencyStatus.value.amount, + cryptoCurrency = cryptoCurrencyStatus.currency.symbol, + decimals = cryptoCurrencyStatus.currency.decimals, ), ), - RoundedListWithDividersItemData( - id = R.string.staking_details_apy, - startText = TextReference.Res(R.string.staking_details_apy), - endText = getAprRange(), - iconClick = { clickIntents.onInfoClick(InfoType.APY) }, - ), - RoundedListWithDividersItemData( - id = R.string.staking_details_on_stake, - startText = TextReference.Res(R.string.staking_details_on_stake), - endText = TextReference.Str( - value = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = (yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero(), - cryptoCurrency = cryptoCurrencyStatus.currency.symbol, - decimals = cryptoCurrencyStatus.currency.decimals, - ), + ) + } + + private fun createApyItem(): RoundedListWithDividersItemData { + return RoundedListWithDividersItemData( + id = R.string.staking_details_apy, + startText = TextReference.Res(R.string.staking_details_apy), + endText = getAprRange(), + iconClick = { clickIntents.onInfoClick(InfoType.APY) }, + ) + } + + private fun createOnStakeItem( + cryptoCurrencyStatus: CryptoCurrencyStatus, + yieldBalance: YieldBalance?, + ): RoundedListWithDividersItemData { + return RoundedListWithDividersItemData( + id = R.string.staking_details_on_stake, + startText = TextReference.Res(R.string.staking_details_on_stake), + endText = TextReference.Str( + value = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = (yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero(), + cryptoCurrency = cryptoCurrencyStatus.currency.symbol, + decimals = cryptoCurrencyStatus.currency.decimals, ), ), - RoundedListWithDividersItemData( - id = R.string.staking_details_unbonding_period, - startText = TextReference.Res(R.string.staking_details_unbonding_period), - endText = TextReference.Str(yield.metadata.cooldownPeriod.days.toString()), - iconClick = { clickIntents.onInfoClick(InfoType.UNBOUNDING_PERIOD) }, + ) + } + + private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData { + return RoundedListWithDividersItemData( + id = R.string.staking_details_unbonding_period, + startText = TextReference.Res(R.string.staking_details_unbonding_period), + endText = pluralReference( + id = R.plurals.common_days, + count = yield.metadata.cooldownPeriod.days, + formatArgs = wrappedList(yield.metadata.cooldownPeriod.days), ), + iconClick = { clickIntents.onInfoClick(InfoType.UNBONDING_PERIOD) }, + ) + } + + private fun createMinimumRequirementItem( + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): RoundedListWithDividersItemData? { + val minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum + return minimumCryptoAmount?.let { RoundedListWithDividersItemData( id = R.string.staking_details_minimum_requirement, startText = TextReference.Res(R.string.staking_details_minimum_requirement), endText = TextReference.Str( value = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum, + cryptoAmount = it, cryptoCurrency = cryptoCurrencyStatus.currency.symbol, decimals = cryptoCurrencyStatus.currency.decimals, ), ), + ) + } + } + + private fun createRewardClaimingItem(): RoundedListWithDividersItemData { + return RoundedListWithDividersItemData( + id = R.string.staking_details_reward_claiming, + startText = TextReference.Res(R.string.staking_details_reward_claiming), + endText = TextReference.Str(yield.metadata.rewardClaiming), + iconClick = { clickIntents.onInfoClick(InfoType.REWARD_CLAIMING) }, + ) + } + + private fun createWarmupPeriodItem(): RoundedListWithDividersItemData { + return RoundedListWithDividersItemData( + id = R.string.staking_details_warmup_period, + startText = TextReference.Res(R.string.staking_details_warmup_period), + endText = pluralReference( + id = R.plurals.common_days, + count = yield.metadata.warmupPeriod.days, + formatArgs = wrappedList(yield.metadata.warmupPeriod.days), ), - RoundedListWithDividersItemData( - id = R.string.staking_details_reward_claiming, - startText = TextReference.Res(R.string.staking_details_reward_claiming), - endText = TextReference.Str(yield.metadata.rewardClaiming), - iconClick = { clickIntents.onInfoClick(InfoType.REWARD_CLAIMING) }, - ), - RoundedListWithDividersItemData( - id = R.string.staking_details_warmup_period, - startText = TextReference.Res(R.string.staking_details_warmup_period), - endText = TextReference.Str(yield.metadata.warmupPeriod.days.toString()), - iconClick = { clickIntents.onInfoClick(InfoType.WARMUP_PERIOD) }, - ), - RoundedListWithDividersItemData( - id = R.string.staking_details_reward_schedule, - startText = TextReference.Res(R.string.staking_details_reward_schedule), - endText = TextReference.Str(yield.metadata.rewardSchedule), - iconClick = { clickIntents.onInfoClick(InfoType.REWARD_SCHEDULE) }, - ), + iconClick = { clickIntents.onInfoClick(InfoType.WARMUP_PERIOD) }, + ) + } + + private fun createRewardScheduleItem(): RoundedListWithDividersItemData { + return RoundedListWithDividersItemData( + id = R.string.staking_details_reward_schedule, + startText = TextReference.Res(R.string.staking_details_reward_schedule), + endText = TextReference.Str(yield.metadata.rewardSchedule), + iconClick = { clickIntents.onInfoClick(InfoType.REWARD_SCHEDULE) }, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt index ff1701bc9b..da8124ccf1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt @@ -22,7 +22,7 @@ internal class ShowInfoBottomSheetStateTransformer( title = resourceReference(R.string.staking_details_apy), text = resourceReference(R.string.staking_details_apy_info), ) - InfoType.UNBOUNDING_PERIOD -> StakingInfoBottomSheetConfig( + InfoType.UNBONDING_PERIOD -> StakingInfoBottomSheetConfig( title = resourceReference(R.string.staking_details_unbonding_period), text = resourceReference(R.string.staking_details_unbonding_period_info), ) @@ -46,7 +46,7 @@ internal class ShowInfoBottomSheetStateTransformer( enum class InfoType { APY, - UNBOUNDING_PERIOD, + UNBONDING_PERIOD, REWARD_CLAIMING, WARMUP_PERIOD, REWARD_SCHEDULE, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index cd1f6aaac1..b0cc41ab8e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -26,7 +26,7 @@ import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImageInfo -import com.tangem.core.ui.components.list.roundedListItems +import com.tangem.core.ui.components.list.roundedListWithDividersItems import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -40,9 +40,12 @@ import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickInten import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.ImmutableList + +// TODO staking metrics block is temporary disabled +// private const val METRICS_BLOCK_KEY = "MetricsBlock" private const val STAKING_REWARD_BLOCK_KEY = "StakingRewardBlock" -private const val METRICS_BLOCK_KEY = "MetricsBlock" private const val ACTIVE_STAKING_BLOCK_KEY = "ActiveStakingBlock" @OptIn(ExperimentalFoundationApi::class) @@ -52,21 +55,25 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl LazyColumn( modifier = Modifier - .background(TangemTheme.colors.background.tertiary) + .background(TangemTheme.colors.background.secondary) .fillMaxSize(), contentPadding = PaddingValues(TangemTheme.dimens.spacing16), ) { - if (state.yieldBalance == InnerYieldBalanceState.Empty) { - item(key = METRICS_BLOCK_KEY) { - Column(modifier = Modifier.animateItemPlacement()) { - MetricsBlock(state) - SpacerH12() - } - } - } + // TODO staking metrics block is temporary disabled + // https://www.figma.com/design/Vs6SkVsFnUPsSCNwlnVf5U?node-id=12484-35755#876661319 + // if (state.yieldBalance == InnerYieldBalanceState.Empty) { + // item(key = METRICS_BLOCK_KEY) { + // Column(modifier = Modifier.animateItemPlacement()) { + // MetricsBlock(state) + // SpacerH12() + // } + // } + // } - this.roundedListItems(state.infoItems) - item { SpacerH12() } + this.roundedListWithDividersItems( + rows = state.infoItems, + footerContent = { SpacerH12() }, + ) if (state.yieldBalance is InnerYieldBalanceState.Data) { item(key = STAKING_REWARD_BLOCK_KEY) { @@ -93,6 +100,7 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl } } +@Suppress("UnusedPrivateMember") @Composable private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) { Column( @@ -193,7 +201,7 @@ private fun StakingRewardBlock( } @Composable -private fun ActiveStakingBlock(groups: List, onClick: (BalanceState) -> Unit) { +private fun ActiveStakingBlock(groups: ImmutableList, onClick: (BalanceState) -> Unit) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index af3698a8b8..d3c1bda086 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -16,6 +16,7 @@ import com.tangem.common.ui.amountScreen.AmountScreenContent import com.tangem.common.ui.navigationButtons.NavigationButtonsBlock import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -74,7 +75,7 @@ private fun SendAppBar(uiState: StakingUiState) { StakingStep.RewardsValidators, StakingStep.Validators, StakingStep.Confirmation, - -> stringResource(id = R.string.common_stake) + -> uiState.title.resolveReference() } val backIcon = when (uiState.currentStep) { StakingStep.Amount, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt index 68d638aaaa..a0c3d67869 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt @@ -1,13 +1,23 @@ package com.tangem.features.staking.impl.presentation.ui.block import androidx.compose.runtime.Composable +import androidx.compose.runtime.key import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.res.TangemTheme import com.tangem.features.staking.impl.presentation.state.StakingNotification +import kotlinx.collections.immutable.ImmutableList @Composable -internal fun NotificationsBlock(notifications: List) { - notifications.forEach { - Notification(config = it.config, iconTint = TangemTheme.colors.icon.accent) +internal fun NotificationsBlock(notifications: ImmutableList) { + notifications.forEach { notification -> + key(notification) { + Notification( + config = notification.config, + iconTint = when (notification) { + is StakingNotification.Error -> TangemTheme.colors.icon.warning + is StakingNotification.Warning -> TangemTheme.colors.icon.accent + }, + ) + } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index a6fcb3d67c..23b5b5b76d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -180,7 +180,10 @@ internal class StakingViewModel @Inject constructor( passthrough = pendingAction?.passthrough, type = pendingAction?.type, ), - ).getOrElse { error("Can't get fee info") } + ).getOrElse { + stateController.update(AddStakingErrorTransformer(it)) + return@launch + } stateController.update( SetConfirmationStateAssentTransformer( From 7b085b4bfcec8637ae1c92e18b6bdf13ecdbabc4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Aug 2024 18:09:43 +0500 Subject: [PATCH 19/87] Updated on 2026-08-14 --- .../data/staking/DefaultStakingRepository.kt | 2 +- .../state/stub/StakingClickIntentsStub.kt | 3 ++- .../SetButtonsStateTransformer.kt | 18 +++++++++++------ .../ui/StakingConfirmationContent.kt | 3 +-- .../ui/StakingInitialInfoContent.kt | 10 ++++++---- .../viewmodel/StakingClickIntents.kt | 8 ++++++-- .../viewmodel/StakingViewModel.kt | 20 +++++++++++-------- 7 files changed, 40 insertions(+), 24 deletions(-) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 6f5ce7909c..c5748f49a8 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -583,7 +583,7 @@ internal class DefaultStakingRepository( // Blockchain.Avalanche.toCoinId() to AVALANCHE_INTEGRATION_ID, // Blockchain.Tron.toCoinId() to TRON_INTEGRATION_ID, // Blockchain.Cronos.toCoinId() to CRONOS_INTEGRATION_ID, - // Blockchain.Binance.toCoinId() to BINANCE_INTEGRATION_ID, + // Blockchain.BSC.toCoinId() to BINANCE_INTEGRATION_ID, // Blockchain.Kava.toCoinId() to KAVA_INTEGRATION_ID, // Blockchain.Near.toCoinId() to NEAR_INTEGRATION_ID, // Blockchain.Tezos.toCoinId() to TEZOS_INTEGRATION_ID, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 8fd79c0d45..edda2d6732 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -2,6 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.stub import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents @@ -11,7 +12,7 @@ object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} - override fun onNextClick(pendingActions: ImmutableList) {} + override fun onNextClick(actionType: StakingActionCommonType?, pendingActions: ImmutableList) {} override fun onActionClick(pendingAction: PendingAction?) {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 9c9d86346f..4332347169 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -119,10 +119,7 @@ internal class SetButtonsStateTransformer : Transformer { private fun List.getSecondaryAction(): PendingAction? = getOrNull(1) private fun StakingUiState.isButtonsVisible(): Boolean = when (currentStep) { - StakingStep.InitialInfo -> { - val initialState = initialInfoState as? StakingStates.InitialInfoState.Data - initialState?.isStakeMoreAvailable == true || initialState?.yieldBalance is InnerYieldBalanceState.Empty - } + StakingStep.InitialInfo -> isStakeMoreAvailable() StakingStep.RewardsValidators -> false else -> true } @@ -164,7 +161,11 @@ internal class SetButtonsStateTransformer : Transformer { private fun StakingUiState.onPrimaryClick() { when (currentStep) { - StakingStep.InitialInfo, + StakingStep.InitialInfo -> { + val actionType = StakingActionCommonType.ENTER.takeIf { isStakeMoreAvailable() } + clickIntents.onAmountValueChange("") // reset amount state + clickIntents.onNextClick(actionType) + } StakingStep.Validators, StakingStep.Amount, -> clickIntents.onNextClick() @@ -187,9 +188,9 @@ internal class SetButtonsStateTransformer : Transformer { StakingStep.InitialInfo, StakingStep.RewardsValidators, StakingStep.Confirmation, + StakingStep.Validators, -> false StakingStep.Amount, - StakingStep.Validators, -> true } @@ -223,4 +224,9 @@ internal class SetButtonsStateTransformer : Transformer { StakingActionType.UNKNOWN -> TextReference.EMPTY null -> TextReference.EMPTY } + + private fun StakingUiState.isStakeMoreAvailable(): Boolean { + val initialState = initialInfoState as? StakingStates.InitialInfoState.Data + return initialState?.isStakeMoreAvailable == true || initialState?.yieldBalance is InnerYieldBalanceState.Empty + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 2fd208acbb..476c27a1ca 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -40,9 +40,8 @@ internal fun StakingConfirmationContent( Column( modifier = Modifier - .fillMaxSize() .background(TangemTheme.colors.background.tertiary) - .padding(TangemTheme.dimens.spacing16) + .padding(horizontal = TangemTheme.dimens.spacing16) .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index b0cc41ab8e..c144d71513 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -33,7 +33,10 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents @@ -56,8 +59,7 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl LazyColumn( modifier = Modifier .background(TangemTheme.colors.background.secondary) - .fillMaxSize(), - contentPadding = PaddingValues(TangemTheme.dimens.spacing16), + .padding(horizontal = TangemTheme.dimens.spacing16), ) { // TODO staking metrics block is temporary disabled // https://www.figma.com/design/Vs6SkVsFnUPsSCNwlnVf5U?node-id=12484-35755#876661319 @@ -186,7 +188,7 @@ private fun StakingRewardBlock( InputRowDefault( title = resourceReference(R.string.staking_rewards), text = text, - iconRes = R.drawable.ic_chevron_right_24, + iconRes = R.drawable.ic_chevron_right_24.takeIf { isRewardsToClaim }, textColor = textColor, modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 9e71013bd6..b5cb61db29 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.viewmodel import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import kotlinx.collections.immutable.ImmutableList @@ -12,7 +13,10 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() - fun onNextClick(pendingActions: ImmutableList = persistentListOf()) + fun onNextClick( + actionType: StakingActionCommonType? = null, + pendingActions: ImmutableList = persistentListOf(), + ) fun onActionClick(pendingAction: PendingAction?) @@ -20,7 +24,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onInfoClick(infoType: InfoType) - override fun onAmountNext() = onNextClick() + override fun onAmountNext() = onNextClick(actionType = null) fun openValidators() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 23b5b5b76d..cdb9cbd9ca 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -102,7 +102,10 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } - override fun onNextClick(pendingActions: ImmutableList) { + override fun onNextClick(actionType: StakingActionCommonType?, pendingActions: ImmutableList) { + if (actionType != null) { + stateController.update { it.copy(actionType = actionType) } + } stakingStateRouter.onNextClick() if (isAssentState()) { estimateGas(pendingActions) @@ -163,6 +166,10 @@ internal class StakingViewModel @Inject constructor( ), ) val cryptoCurrencyValue = cryptoCurrencyStatus.value + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + ?: error("No confirmation state") + val validatorState = confirmationState.validatorState as? ValidatorState.Content + ?: error("No validator provided") val pendingAction = pendingActions.firstOrNull() val stakingGasEstimate = estimateGasUseCase( @@ -175,7 +182,7 @@ internal class StakingViewModel @Inject constructor( ?: error("No amount provided"), address = cryptoCurrencyValue.networkAddress?.defaultAddress?.value ?: error("No available address"), - validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"), + validatorAddress = validatorState.chosenValidator.address, token = yield.token, passthrough = pendingAction?.passthrough, type = pendingAction?.type, @@ -230,10 +237,7 @@ internal class StakingViewModel @Inject constructor( stateController.update(ValidatorSelectChangeTransformer(validator)) } - override fun openRewardsValidators() { - stateController.update { it.copy(actionType = StakingActionCommonType.PENDING_REWARDS) } - onNextClick() - } + override fun openRewardsValidators() = onNextClick(actionType = StakingActionCommonType.PENDING_REWARDS) override fun onActiveStake(activeStake: BalanceState) { val actionType = if (activeStake.pendingActions.isEmpty()) { @@ -241,9 +245,9 @@ internal class StakingViewModel @Inject constructor( } else { StakingActionCommonType.PENDING_OTHER } - stateController.update { it.copy(actionType = actionType) } + stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, activeStake.cryptoValue)) - onNextClick(activeStake.pendingActions) + onNextClick(actionType, activeStake.pendingActions) } override fun onExploreClick() { From 836fa6f4aba6f6b6a8b4f9acb93a571c9773a1fc Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Aug 2024 15:19:13 +0400 Subject: [PATCH 20/87] Updated on 2026-08-14 --- features/manage-tokens/api/build.gradle.kts | 3 +++ .../component/AddCustomTokenComponent.kt | 18 ++++++++++++++++++ features/manage-tokens/impl/build.gradle.kts | 5 +++++ gradle/dependencies.toml | 1 + .../extension/BaseExtensionConfigurations.kt | 1 + 5 files changed, 28 insertions(+) create mode 100644 features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt diff --git a/features/manage-tokens/api/build.gradle.kts b/features/manage-tokens/api/build.gradle.kts index 0cf94d3840..8bbbd4e02b 100644 --- a/features/manage-tokens/api/build.gradle.kts +++ b/features/manage-tokens/api/build.gradle.kts @@ -15,4 +15,7 @@ dependencies { /* Project - Core */ implementation(projects.core.ui) implementation(projects.core.decompose) + + /* Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt new file mode 100644 index 0000000000..761758d465 --- /dev/null +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt @@ -0,0 +1,18 @@ +package com.tangem.features.managetokens.component + +import androidx.compose.runtime.Composable +import com.tangem.domain.wallets.models.UserWalletId + +interface AddCustomTokenComponent { + + @Composable + fun BottomSheet(isVisible: Boolean, onDismiss: () -> Unit) + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory { + fun create(params: Params): AddCustomTokenComponent + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 05647d3743..1305e90261 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -21,6 +21,10 @@ dependencies { implementation(projects.common.routing) implementation(projects.core.featuretoggles) + /* Project - Domain */ + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) + /* AndroidX */ implementation(deps.androidx.activity.compose) implementation(deps.lifecycle.compose) @@ -29,6 +33,7 @@ dependencies { implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) implementation(deps.compose.foundation) + implementation(deps.compose.material) // For button colors implementation(deps.compose.material3) implementation(deps.compose.shimmer) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 5dd3fc5ba9..ba5cf565d8 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -155,6 +155,7 @@ androidx-datastore = { module = "androidx.datastore:datastore-preferences", vers # region AndroidX # region Compose +compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose-runtime" } compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-runtime" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-runtime" } compose-ui-utils = { module = "androidx.compose.ui:ui-util", version.ref = "compose-runtime" } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index 49a6869c47..fe2f2c147f 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -25,6 +25,7 @@ internal fun BaseExtension.configureCompose(project: Project) { contains(Regex(pattern = ":presentation\$")) || contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA] contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function + contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function contains(Regex(pattern = ":impl\$")) } From a87c618ec6ad054b8f2f61f42e02b11510189c73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Aug 2024 15:19:57 +0400 Subject: [PATCH 21/87] Updated on 2026-08-14 --- .../ui/components/fields/SimpleTextField.kt | 65 +++++++++---------- .../com/tangem/core/ui/res/TangemTheme.kt | 14 ++-- 2 files changed, 38 insertions(+), 41 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 59128a93a9..c37307f136 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 @@ -1,12 +1,11 @@ package com.tangem.core.ui.components.fields import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.selection.LocalTextSelectionColors -import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -40,6 +39,7 @@ fun SimpleTextField( textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), placeholderColor: Color = TangemTheme.colors.text.disabled, readOnly: Boolean = false, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, isValuePasted: Boolean = false, onValuePastedTriggerDismiss: () -> Unit = {}, decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null, @@ -54,10 +54,6 @@ fun SimpleTextField( ) } val focusRequester = remember { FocusRequester.Default } - val customTextSelectionColors = TextSelectionColors( - handleColor = TangemTheme.colors.text.accent, - backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f), - ) val textFieldValue = textFieldValueState.copy(text = value) var lastTextValue by remember(proxyValue, isValuePasted) { textFieldValueState = textFieldValueState.copy( @@ -85,37 +81,36 @@ fun SimpleTextField( } } - CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) { - BasicTextField( - value = textFieldValue, - onValueChange = { newTextFieldValueState -> - textFieldValueState = newTextFieldValueState + BasicTextField( + value = textFieldValue, + onValueChange = { newTextFieldValueState -> + textFieldValueState = newTextFieldValueState - val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text - lastTextValue = newTextFieldValueState.text + val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text + lastTextValue = newTextFieldValueState.text - if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text) - }, - textStyle = textStyle.copy(color = color), - cursorBrush = SolidColor(TangemTheme.colors.text.primary1), - singleLine = singleLine, - readOnly = readOnly, - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - decorationBox = decorationBox ?: { textValue -> - SimpleTextPlaceholder( - placeholder = placeholder, - value = value, - textStyle = textStyle, - textValue = textValue, - color = placeholderColor, - ) - }, - modifier = modifier - .focusRequester(focusRequester), - ) - } + if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text) + }, + textStyle = textStyle.copy(color = color), + cursorBrush = SolidColor(TangemTheme.colors.text.primary1), + singleLine = singleLine, + readOnly = readOnly, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + decorationBox = decorationBox ?: { textValue -> + SimpleTextPlaceholder( + placeholder = placeholder, + value = value, + textStyle = textStyle, + textValue = textValue, + color = placeholderColor, + ) + }, + modifier = modifier + .focusRequester(focusRequester), + ) } @Composable diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index f62345873b..65ffdd461b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -52,11 +52,11 @@ fun TangemTheme( LocalHapticManager provides hapticManager, LocalSnackbarHostState provides snackbarHostState, LocalWindowSize provides windowSize, - LocalTextSelectionColors provides TangemTextSelectionColors, ) { CompositionLocalProvider( LocalTangemShimmer provides TangemShimmer, LocalMainBottomSheetColor provides remember { mutableStateOf(Color.Unspecified) }, + LocalTextSelectionColors provides TangemTextSelectionColors, ) { ProvideTextStyle( value = TangemTheme.typography.body1, @@ -209,11 +209,13 @@ private fun darkThemeColors(): TangemColors { ) } -@Stable -private val TangemTextSelectionColors = TextSelectionColors( - handleColor = TangemColorPalette.Azure, - backgroundColor = TangemColorPalette.Azure.copy(alpha = 0.4f), -) +private val TangemTextSelectionColors: TextSelectionColors + @Composable + @ReadOnlyComposable + get() = TextSelectionColors( + handleColor = TangemTheme.colors.text.accent, + backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f), + ) private val LocalTangemColors = staticCompositionLocalOf { error("No TangemColors provided") From 6e67390a0d307bfc49f60e73980070276ee4067b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Aug 2024 15:21:33 +0400 Subject: [PATCH 22/87] Updated on 2026-08-14 --- .../preview/PreviewManageTokensComponent.kt | 28 ++++++++----------- .../managetokens/entity/CurrencyNetworkUM.kt | 9 ++++-- .../managetokens/ui/ManageTokensScreen.kt | 11 +++++++- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index 8fabf7cfd4..6923cb7136 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -8,9 +8,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.entity.CurrencyItemUM import com.tangem.features.managetokens.entity.CurrencyNetworkUM @@ -129,14 +129,11 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex -> CurrencyNetworkUM( - id = networkIndex.toString(), - model = BlockchainRowUM( - name = "NETWORK$networkIndex", - type = "N$networkIndex", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = networkIndex == 0, - isSelected = false, - ), + id = Network.ID(networkIndex.toString()), + name = "NETWORK$networkIndex", + type = "N$networkIndex", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = networkIndex == 0, isSelected = false, onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) }, ) @@ -171,14 +168,11 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { it.fastForEachIndexed { index, network -> if (index == networkIndex) { it[index] = network.copy( - model = network.model.copy( - iconResId = if (isSelected) { - R.drawable.img_eth_22 - } else { - R.drawable.ic_eth_16 - }, - isSelected = isSelected, - ), + iconResId = if (isSelected) { + R.drawable.img_eth_22 + } else { + R.drawable.ic_eth_16 + }, isSelected = isSelected, ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt index 8ed8dc5f9d..477585db3f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt @@ -1,12 +1,15 @@ package com.tangem.features.managetokens.entity import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.domain.tokens.model.Network @Immutable internal data class CurrencyNetworkUM( - val id: String, - val model: BlockchainRowUM, + val id: Network.ID, + val name: String, + val type: String, + val iconResId: Int, + val isMainNetwork: Boolean, val isSelected: Boolean, val onSelectedStateChange: (Boolean) -> Unit, ) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 63a14af8d4..e21347f830 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -35,6 +35,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.rows.ArrowRow import com.tangem.core.ui.components.rows.BlockchainRow import com.tangem.core.ui.components.rows.ChainRow +import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -245,7 +246,15 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod isLastItem = index == currentItems.lastIndex, content = { BlockchainRow( - model = network.model, + model = with(network) { + BlockchainRowUM( + name = name, + type = type, + iconResId = iconResId, + isMainNetwork = isMainNetwork, + isSelected = isSelected, + ) + }, action = { TangemSwitch( checked = network.isSelected, From 2aea20e5977f81657d3567807bd6db0094e422c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Aug 2024 15:21:44 +0400 Subject: [PATCH 23/87] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Keyboard.kt | 7 +- .../component/CustomTokenFormComponent.kt | 19 ++ .../CustomTokenNetworkSelectorComponent.kt | 20 ++ .../preview/PreviewAddCustomTokenComponent.kt | 81 ++++++++ .../PreviewCustomTokenFormComponent.kt | 81 ++++++++ ...viewCustomTokenNetworkSelectorComponent.kt | 49 +++++ .../managetokens/entity/AddCustomTokenUM.kt | 50 +++++ .../managetokens/entity/CustomTokenFormUM.kt | 44 ++++ .../entity/CustomTokenNetworkSelectorUM.kt | 9 + .../ui/AddCustomTokenBottomSheet.kt | 148 +++++++++++++ .../managetokens/ui/CustomTokenFormContent.kt | 196 ++++++++++++++++++ .../ui/CustomTokenNetworkSelectorContent.kt | 125 +++++++++++ 12 files changed, 828 insertions(+), 1 deletion(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenNetworkSelectorComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenNetworkSelectorComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/AddCustomTokenUM.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenFormUM.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenNetworkSelectorUM.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt index a920fda100..72a9f3a11e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt @@ -3,6 +3,7 @@ package com.tangem.core.ui.components import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.ime import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.platform.LocalDensity @@ -14,11 +15,15 @@ sealed interface Keyboard { data class Opened(override val height: Dp) : Keyboard - object Closed : Keyboard { + data object Closed : Keyboard { override val height: Dp = 0.dp } } +val Keyboard.isOpened: Boolean + @Stable + get() = this is Keyboard.Opened + /** * Allows to subscribe to a soft keyboard to detect when it's open/closed */ diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt new file mode 100644 index 0000000000..acb8dc42a4 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt @@ -0,0 +1,19 @@ +package com.tangem.features.managetokens.component + +import androidx.compose.foundation.lazy.LazyListScope +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +internal interface CustomTokenFormComponent { + + fun content(scope: LazyListScope) + + data class Params( + val userWalletId: UserWalletId, + val networkId: Network.ID, + ) + + interface Factory { + fun create(params: Params): CustomTokenFormComponent + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenNetworkSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenNetworkSelectorComponent.kt new file mode 100644 index 0000000000..a2551d430c --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenNetworkSelectorComponent.kt @@ -0,0 +1,20 @@ +package com.tangem.features.managetokens.component + +import androidx.compose.foundation.lazy.LazyListScope +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.entity.SelectedNetworkUM + +internal interface CustomTokenNetworkSelectorComponent { + + fun content(scope: LazyListScope) + + data class Params( + val userWalletId: UserWalletId, + val selectedNetwork: SelectedNetworkUM?, + val onNetworkSelected: (SelectedNetworkUM) -> Unit, + ) + + interface Factory { + fun create(params: Params): CustomTokenNetworkSelectorComponent + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt new file mode 100644 index 0000000000..a5d8b9f55b --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt @@ -0,0 +1,81 @@ +package com.tangem.features.managetokens.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.component.AddCustomTokenComponent +import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent +import com.tangem.features.managetokens.entity.AddCustomTokenButtonUM +import com.tangem.features.managetokens.entity.AddCustomTokenUM +import com.tangem.features.managetokens.entity.ClickableFieldUM +import com.tangem.features.managetokens.entity.SelectedNetworkUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +internal class PreviewAddCustomTokenComponent( + initialState: AddCustomTokenUM = AddCustomTokenUM.NetworkSelector(), +) : AddCustomTokenComponent { + + private val userWalletId = UserWalletId(stringValue = "321") + + private val previewState: MutableStateFlow = MutableStateFlow(initialState) + + @Composable + override fun BottomSheet(isVisible: Boolean, onDismiss: () -> Unit) { + val state by previewState.collectAsStateWithLifecycle() + val config = TangemBottomSheetConfig( + isShow = isVisible, + onDismissRequest = onDismiss, + content = state, + ) + + AddCustomTokenBottomSheet( + config = config, + content = { + when (val s = state) { + is AddCustomTokenUM.Form -> { + PreviewCustomTokenFormComponent( + networkName = ClickableFieldUM( + label = resourceReference(R.string.custom_token_network_input_title), + value = stringReference(s.selectedNetwork.name), + onClick = { showNetworkSelector(s.selectedNetwork) }, + ), + ).content(this) + } + is AddCustomTokenUM.NetworkSelector -> { + PreviewCustomTokenNetworkSelectorComponent( + params = CustomTokenNetworkSelectorComponent.Params( + userWalletId = userWalletId, + selectedNetwork = s.selectedNetwork, + onNetworkSelected = ::showForm, + ), + networksSize = 20, + ).content(this) + } + } + }, + ) + } + + private fun showNetworkSelector(selectedNetwork: SelectedNetworkUM) { + previewState.update { AddCustomTokenUM.NetworkSelector(selectedNetwork) } + } + + private fun showForm(network: SelectedNetworkUM) { + previewState.update { + AddCustomTokenUM.Form( + selectedNetwork = network, + addTokenButton = AddCustomTokenButtonUM.Visible( + isEnabled = false, + onClick = {}, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt new file mode 100644 index 0000000000..625090f28d --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt @@ -0,0 +1,81 @@ +package com.tangem.features.managetokens.component.preview + +import androidx.compose.foundation.lazy.LazyListScope +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.managetokens.component.CustomTokenFormComponent +import com.tangem.features.managetokens.entity.ClickableFieldUM +import com.tangem.features.managetokens.entity.CustomTokenFormUM +import com.tangem.features.managetokens.entity.TextInputFieldUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.ui.customTokenFormContent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal class PreviewCustomTokenFormComponent( + networkName: ClickableFieldUM = ClickableFieldUM( + label = resourceReference(R.string.custom_token_network_input_title), + value = stringReference(value = "Ethereum"), + onClick = {}, + ), + derivationPath: ClickableFieldUM = ClickableFieldUM( + label = resourceReference(R.string.custom_token_derivation_path), + value = stringReference(value = "Default"), + onClick = {}, + ), + canAddToken: Boolean = false, + contractAddress: TextInputFieldUM = TextInputFieldUM( + label = resourceReference(R.string.custom_token_contract_address_input_title), + placeholder = stringReference(value = "0x000000000000000000000000000"), + value = "", + onValueChange = {}, + ), + tokenName: TextInputFieldUM = TextInputFieldUM( + label = resourceReference(R.string.custom_token_name_input_title), + placeholder = stringReference(value = "E.g. USD Coin"), + value = "", + onValueChange = {}, + ), + tokenSymbol: TextInputFieldUM = TextInputFieldUM( + label = resourceReference(R.string.custom_token_token_symbol_input_title), + placeholder = stringReference(value = "E.g. USDC"), + value = "", + onValueChange = {}, + ), + tokenDecimals: TextInputFieldUM = TextInputFieldUM( + label = resourceReference(R.string.custom_token_decimals_input_title), + placeholder = stringReference(value = "8"), + value = "", + onValueChange = {}, + ), + notifications: ImmutableList = persistentListOf( + CustomTokenFormUM.NotificationUM( + id = "1", + config = NotificationConfig( + title = stringReference(value = "Note that tokens can be created by anyone"), + subtitle = stringReference(value = "Be aware of adding scam tokens, they can cost nothing"), + iconResId = R.drawable.img_attention_20, + ), + ), + ), +) : CustomTokenFormComponent { + + private val previewState = CustomTokenFormUM( + networkName = networkName, + contractAddress = contractAddress, + tokenName = tokenName, + tokenSymbol = tokenSymbol, + tokenDecimals = tokenDecimals, + derivationPath = derivationPath, + notifications = notifications, + canAddToken = canAddToken, + onDerivationPathClick = {}, + onNetworkClick = {}, + onAddClick = {}, + ) + + override fun content(scope: LazyListScope) { + scope.customTokenFormContent(model = previewState) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenNetworkSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenNetworkSelectorComponent.kt new file mode 100644 index 0000000000..a5713bce2c --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenNetworkSelectorComponent.kt @@ -0,0 +1,49 @@ +package com.tangem.features.managetokens.component.preview + +import androidx.compose.foundation.lazy.LazyListScope +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent +import com.tangem.features.managetokens.entity.CurrencyNetworkUM +import com.tangem.features.managetokens.entity.CustomTokenNetworkSelectorUM +import com.tangem.features.managetokens.entity.SelectedNetworkUM +import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.ui.customTokenNetworkSelectorContent +import kotlinx.collections.immutable.toImmutableList + +internal class PreviewCustomTokenNetworkSelectorComponent( + private val params: CustomTokenNetworkSelectorComponent.Params = CustomTokenNetworkSelectorComponent.Params( + userWalletId = UserWalletId(stringValue = "321"), + selectedNetwork = null, + onNetworkSelected = {}, + ), + networksSize: Int = 5, +) : CustomTokenNetworkSelectorComponent { + + private val previewNetworks = List(size = networksSize) { networkIndex -> + val n = SelectedNetworkUM( + id = Network.ID(networkIndex.toString()), + name = "Network $networkIndex", + ) + + CurrencyNetworkUM( + id = n.id, + name = n.name, + type = "N$networkIndex", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = false, + isSelected = n.id == params.selectedNetwork?.id, + onSelectedStateChange = { params.onNetworkSelected(n) }, + ) + }.toImmutableList() + + private val previewState = CustomTokenNetworkSelectorUM( + networks = previewNetworks, + ) + + override fun content(scope: LazyListScope) { + scope.customTokenNetworkSelectorContent( + model = previewState, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/AddCustomTokenUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/AddCustomTokenUM.kt new file mode 100644 index 0000000000..c4d4cec789 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/AddCustomTokenUM.kt @@ -0,0 +1,50 @@ +package com.tangem.features.managetokens.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.domain.tokens.model.Network + +@Immutable +internal sealed class AddCustomTokenUM : TangemBottomSheetConfigContent { + + abstract val selectedNetwork: SelectedNetworkUM? + abstract val addTokenButton: AddCustomTokenButtonUM + + data class NetworkSelector( + override val selectedNetwork: SelectedNetworkUM? = null, + ) : AddCustomTokenUM() { + + override val addTokenButton: AddCustomTokenButtonUM = AddCustomTokenButtonUM.Hidden + } + + data class Form( + override val selectedNetwork: SelectedNetworkUM, + override val addTokenButton: AddCustomTokenButtonUM.Visible, + ) : AddCustomTokenUM() +} + +@Immutable +internal data class SelectedNetworkUM( + val id: Network.ID, + val name: String, +) + +@Immutable +internal sealed class AddCustomTokenButtonUM { + + open val onClick: () -> Unit = {} + + open val isEnabled: Boolean = false + + val isVisible: Boolean + get() = this is Visible + + data object Hidden : AddCustomTokenButtonUM() { + override val onClick: () -> Unit = {} + } + + data class Visible( + override val isEnabled: Boolean, + override val onClick: () -> Unit, + ) : AddCustomTokenButtonUM() +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenFormUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenFormUM.kt new file mode 100644 index 0000000000..ea9bfbceb0 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenFormUM.kt @@ -0,0 +1,44 @@ +package com.tangem.features.managetokens.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal data class CustomTokenFormUM( + val networkName: ClickableFieldUM, + val contractAddress: TextInputFieldUM, + val tokenName: TextInputFieldUM, + val tokenSymbol: TextInputFieldUM, + val tokenDecimals: TextInputFieldUM, + val derivationPath: ClickableFieldUM, + val notifications: ImmutableList, + val canAddToken: Boolean, + val onNetworkClick: () -> Unit, + val onDerivationPathClick: () -> Unit, + val onAddClick: () -> Unit, +) { + + @Immutable + data class NotificationUM( + val id: String, + val config: NotificationConfig, + ) +} + +@Immutable +internal data class TextInputFieldUM( + val label: TextReference, + val placeholder: TextReference, + val value: String, + val onValueChange: (String) -> Unit, + val error: TextReference? = null, +) + +@Immutable +internal data class ClickableFieldUM( + val label: TextReference, + val value: TextReference, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenNetworkSelectorUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenNetworkSelectorUM.kt new file mode 100644 index 0000000000..4d8480eee3 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenNetworkSelectorUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.managetokens.entity + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal data class CustomTokenNetworkSelectorUM( + val networks: ImmutableList, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt new file mode 100644 index 0000000000..a955df759a --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt @@ -0,0 +1,148 @@ +package com.tangem.features.managetokens.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +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 androidx.compose.ui.unit.dp +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.components.isOpened +import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.component.AddCustomTokenComponent +import com.tangem.features.managetokens.component.preview.PreviewAddCustomTokenComponent +import com.tangem.features.managetokens.entity.AddCustomTokenButtonUM +import com.tangem.features.managetokens.entity.AddCustomTokenUM +import com.tangem.features.managetokens.entity.SelectedNetworkUM +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig, content: LazyListScope.() -> Unit) { + TangemBottomSheet( + config = config, + titleText = resourceReference(R.string.add_custom_token_title), + containerColor = TangemTheme.colors.background.secondary, + content = { model: AddCustomTokenUM -> + Content( + model = model, + content = content, + ) + }, + ) +} + +@Composable +private fun Content(model: AddCustomTokenUM, content: LazyListScope.() -> Unit, modifier: Modifier = Modifier) { + val density = LocalDensity.current + val keyboardState by keyboardAsState() + + var fabHeight by remember { mutableStateOf(0.dp) } + + Scaffold( + modifier = modifier.imePadding(), + containerColor = TangemTheme.colors.background.secondary, + floatingActionButtonPosition = FabPosition.Center, + floatingActionButton = { + AnimatedVisibility( + modifier = Modifier.onSizeChanged { + fabHeight = with(density) { it.height.toDp() } + }, + visible = model.addTokenButton.isVisible && !keyboardState.isOpened, + enter = fadeIn(), + exit = fadeOut(), + label = "Add button visibility", + ) { + PrimaryButton( + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing16) + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + text = stringResource(id = R.string.custom_token_add_token), + enabled = model.addTokenButton.isEnabled, + onClick = model.addTokenButton.onClick, + ) + } + }, + ) { paddingValues -> + LazyColumn( + modifier = Modifier.padding(paddingValues), + contentPadding = PaddingValues( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing32 + fabHeight, + ), + ) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = TangemTheme.dimens.spacing16), + contentAlignment = Alignment.Center, + ) { + Text( + modifier = Modifier.fillMaxWidth(fraction = 0.7f), + text = stringResource(id = R.string.custom_token_subtitle), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } + } + + content() + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_AddCustomTokenBottomSheet( + @PreviewParameter(AddCustomTokenComponentPreviewProvider::class) component: AddCustomTokenComponent, +) { + TangemThemePreview { + component.BottomSheet(isVisible = true, onDismiss = {}) + } +} + +private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + PreviewAddCustomTokenComponent(), + PreviewAddCustomTokenComponent( + initialState = AddCustomTokenUM.Form( + selectedNetwork = SelectedNetworkUM( + id = Network.ID(value = "1"), + name = "Ethereum", + ), + addTokenButton = AddCustomTokenButtonUM.Visible( + isEnabled = false, + onClick = {}, + ), + ), + ), + ) +} +// endregion Preview \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt new file mode 100644 index 0000000000..e898e44150 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt @@ -0,0 +1,196 @@ +package com.tangem.features.managetokens.ui + +import android.content.res.Configuration +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +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.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +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.block.information.InformationBlock +import com.tangem.core.ui.components.fields.SimpleTextField +import com.tangem.core.ui.components.notifications.Notification +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.res.TangemThemePreview +import com.tangem.features.managetokens.component.preview.PreviewCustomTokenFormComponent +import com.tangem.features.managetokens.entity.ClickableFieldUM +import com.tangem.features.managetokens.entity.CustomTokenFormUM +import com.tangem.features.managetokens.entity.TextInputFieldUM + +internal fun LazyListScope.customTokenFormContent(model: CustomTokenFormUM) { + item { + ClickableField( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + model = model.networkName, + ) + } + + item { + Column( + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing12) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) { + TextField( + model = model.contractAddress, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + ), + ) + TextField( + model = model.tokenName, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + ), + ) + TextField( + model = model.tokenSymbol, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + ), + ) + TextField( + model = model.tokenDecimals, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Decimal, + imeAction = ImeAction.Next, + ), + ) + } + } + + item { + ClickableField( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + model = model.derivationPath, + ) + } + + items( + items = model.notifications, + key = { it.id }, + ) { notification -> + Notification( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + config = notification.config, + containerColor = TangemTheme.colors.button.disabled, + ) + } +} + +@Composable +private fun TextField( + model: TextInputFieldUM, + modifier: Modifier = Modifier, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, +) { + InformationBlock( + modifier = modifier, + title = { + val color by animateColorAsState( + targetValue = if (model.error != null) { + TangemTheme.colors.text.warning + } else { + TangemTheme.colors.text.tertiary + }, + label = "Field label color", + ) + + Text( + text = (model.error ?: model.label).resolveReference(), + style = TangemTheme.typography.subtitle2, + color = color, + ) + }, + content = { + SimpleTextField( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + value = model.value, + onValueChange = model.onValueChange, + readOnly = false, + placeholder = model.placeholder, + singleLine = true, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + }, + ) +} + +@Composable +private fun ClickableField(model: ClickableFieldUM, modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable(onClick = model.onClick), + title = { + Text( + text = model.label.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + content = { + Text( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + text = model.value.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + }, + ) +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_CustomTokenFormContent( + @PreviewParameter(PreviewCustomTokenFormComponentProvider::class) + component: PreviewCustomTokenFormComponent, +) { + TangemThemePreview { + LazyColumn( + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + ) { component.content(scope = this) } + } +} + +private class PreviewCustomTokenFormComponentProvider : + PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + PreviewCustomTokenFormComponent(), + PreviewCustomTokenFormComponent( + contractAddress = TextInputFieldUM( + label = stringReference("Contract address"), + value = "0x1234567890", + error = stringReference("Contract address is invalid"), + placeholder = stringReference("0x1234567890"), + onValueChange = {}, + ), + ), + ) +} +// endregion Preview \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt new file mode 100644 index 0000000000..10e6f1f107 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt @@ -0,0 +1,125 @@ +package com.tangem.features.managetokens.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +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.itemsIndexed +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.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.rows.ChainRow +import com.tangem.core.ui.components.rows.model.ChainRowUM +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.managetokens.component.preview.PreviewCustomTokenNetworkSelectorComponent +import com.tangem.features.managetokens.entity.CurrencyNetworkUM +import com.tangem.features.managetokens.entity.CustomTokenNetworkSelectorUM +import com.tangem.features.managetokens.impl.R + +internal fun LazyListScope.customTokenNetworkSelectorContent(model: CustomTokenNetworkSelectorUM) { + val lastIndex = model.networks.lastIndex + + item { + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size36) + .background( + color = TangemTheme.colors.background.primary, + shape = TangemTheme.shapes.bottomSheet, + ), + ) { + Text( + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing6, + ) + .padding(horizontal = TangemTheme.dimens.spacing12), + text = "Choose network", + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + + itemsIndexed( + items = model.networks, + key = { _, item -> item.id.value }, + ) { index, item -> + NetworkItem( + modifier = Modifier + .fillMaxWidth() + .clip( + shape = if (index == lastIndex) { + RoundedCornerShape( + bottomStart = TangemTheme.dimens.radius16, + bottomEnd = TangemTheme.dimens.radius16, + ) + } else { + RectangleShape + }, + ) + .background(color = TangemTheme.colors.background.primary) + .clickable(onClick = { item.onSelectedStateChange(true) }) + .padding(horizontal = TangemTheme.dimens.spacing4), + model = item, + ) + } +} + +@Composable +private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) { + ChainRow( + modifier = modifier, + model = with(model) { + ChainRowUM( + name = name, + type = type, + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = model.iconResId, + isGrayscale = false, + showCustomBadge = false, + ), + showCustom = false, + ) + }, + action = { + AnimatedVisibility( + modifier = Modifier.size(TangemTheme.dimens.size24), + visible = model.isSelected, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + }, + ) +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_CustomTokenNetworkSelectorContent() { + TangemThemePreview { + LazyColumn { + PreviewCustomTokenNetworkSelectorComponent().content(this) + } + } +} +// endregion Preview \ No newline at end of file From 3315c1b92aec64cbcb016b98a2f599cf852d0d99 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Aug 2024 19:36:30 +0400 Subject: [PATCH 24/87] Updated on 2026-08-14 --- .../core/ui/components/bottomsheets/TangemBottomSheet.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 67466771ba..388e6b69c2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible @@ -114,6 +115,7 @@ inline fun PreviewBottomSheet( crossinline content: @Composable (ColumnScope.(T) -> Unit), ) { BasicBottomSheet( + modifier = Modifier.width(360.dp), config = config, sheetState = SheetState( skipPartiallyExpanded = true, @@ -137,6 +139,7 @@ inline fun BasicBottomSheet( addBottomInsets: Boolean, crossinline title: @Composable (BoxScope.(T) -> Unit), crossinline content: @Composable (ColumnScope.(T) -> Unit), + modifier: Modifier = Modifier, ) { val model = config.content as? T ?: return @@ -145,7 +148,7 @@ inline fun BasicBottomSheet( ModalBottomSheet( // FIXME temporary solution to fix height of the bottom sheet - modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight), + modifier = modifier.heightIn(max = LocalWindowSize.current.height - statusBarHeight), onDismissRequest = config.onDismissRequest, sheetState = sheetState, containerColor = containerColor, From f599b2f4ce4bb3c22f422b861c847342cb5bf105 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Aug 2024 20:14:43 +0400 Subject: [PATCH 25/87] Updated on 2026-08-14 --- .../preview/PreviewAddCustomTokenComponent.kt | 7 +- ...viewCustomTokenNetworkSelectorComponent.kt | 1 + .../managetokens/entity/AddCustomTokenUM.kt | 4 + .../entity/CustomTokenNetworkSelectorUM.kt | 1 + .../ui/AddCustomTokenBottomSheet.kt | 72 ++++++++++++---- .../ui/CustomTokenNetworkSelectorContent.kt | 85 +++++++++++++------ 6 files changed, 126 insertions(+), 44 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt index a5d8b9f55b..b428c0ed2d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update internal class PreviewAddCustomTokenComponent( - initialState: AddCustomTokenUM = AddCustomTokenUM.NetworkSelector(), + initialState: AddCustomTokenUM = AddCustomTokenUM.NetworkSelector(popBack = {}), ) : AddCustomTokenComponent { private val userWalletId = UserWalletId(stringValue = "321") @@ -64,12 +64,15 @@ internal class PreviewAddCustomTokenComponent( } private fun showNetworkSelector(selectedNetwork: SelectedNetworkUM) { - previewState.update { AddCustomTokenUM.NetworkSelector(selectedNetwork) } + previewState.update { + AddCustomTokenUM.NetworkSelector(selectedNetwork, popBack = { showForm(selectedNetwork) }) + } } private fun showForm(network: SelectedNetworkUM) { previewState.update { AddCustomTokenUM.Form( + popBack = {}, selectedNetwork = network, addTokenButton = AddCustomTokenButtonUM.Visible( isEnabled = false, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenNetworkSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenNetworkSelectorComponent.kt index a5713bce2c..620dbe1222 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenNetworkSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenNetworkSelectorComponent.kt @@ -38,6 +38,7 @@ internal class PreviewCustomTokenNetworkSelectorComponent( }.toImmutableList() private val previewState = CustomTokenNetworkSelectorUM( + showTitle = params.selectedNetwork == null, networks = previewNetworks, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/AddCustomTokenUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/AddCustomTokenUM.kt index c4d4cec789..a34939d61f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/AddCustomTokenUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/AddCustomTokenUM.kt @@ -10,8 +10,11 @@ internal sealed class AddCustomTokenUM : TangemBottomSheetConfigContent { abstract val selectedNetwork: SelectedNetworkUM? abstract val addTokenButton: AddCustomTokenButtonUM + abstract val popBack: () -> Unit + data class NetworkSelector( override val selectedNetwork: SelectedNetworkUM? = null, + override val popBack: () -> Unit, ) : AddCustomTokenUM() { override val addTokenButton: AddCustomTokenButtonUM = AddCustomTokenButtonUM.Hidden @@ -20,6 +23,7 @@ internal sealed class AddCustomTokenUM : TangemBottomSheetConfigContent { data class Form( override val selectedNetwork: SelectedNetworkUM, override val addTokenButton: AddCustomTokenButtonUM.Visible, + override val popBack: () -> Unit, ) : AddCustomTokenUM() } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenNetworkSelectorUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenNetworkSelectorUM.kt index 4d8480eee3..1435d25f7d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenNetworkSelectorUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CustomTokenNetworkSelectorUM.kt @@ -5,5 +5,6 @@ import kotlinx.collections.immutable.ImmutableList @Immutable internal data class CustomTokenNetworkSelectorUM( + val showTitle: Boolean, val networks: ImmutableList, ) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt index a955df759a..a002b54b4c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt @@ -22,8 +22,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle import com.tangem.core.ui.components.isOpened import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.resourceReference @@ -39,11 +43,13 @@ import com.tangem.features.managetokens.impl.R @Composable internal fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig, content: LazyListScope.() -> Unit) { - TangemBottomSheet( + TangemBottomSheet( config = config, - titleText = resourceReference(R.string.add_custom_token_title), + title = { model -> + Title(model) + }, containerColor = TangemTheme.colors.background.secondary, - content = { model: AddCustomTokenUM -> + content = { model -> Content( model = model, content = content, @@ -52,6 +58,26 @@ internal fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig, content: ) } +@Composable +private fun Title(model: AddCustomTokenUM, modifier: Modifier = Modifier) { + val showTokenNetworkTitle = model is AddCustomTokenUM.NetworkSelector && model.selectedNetwork != null + + if (showTokenNetworkTitle) { + TangemTopAppBar( + modifier = modifier, + title = resourceReference(R.string.custom_token_network_selector_title), + titleAlignment = Alignment.CenterHorizontally, + startButton = TopAppBarButtonUM.Back(model.popBack), + height = TangemTopAppBarHeight.BOTTOM_SHEET, + ) + } else { + TangemBottomSheetTitle( + modifier = modifier, + title = resourceReference(R.string.add_custom_token_title), + ) + } +} + @Composable private fun Content(model: AddCustomTokenUM, content: LazyListScope.() -> Unit, modifier: Modifier = Modifier) { val density = LocalDensity.current @@ -94,19 +120,23 @@ private fun Content(model: AddCustomTokenUM, content: LazyListScope.() -> Unit, ), ) { item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = TangemTheme.dimens.spacing16), - contentAlignment = Alignment.Center, - ) { - Text( - modifier = Modifier.fillMaxWidth(fraction = 0.7f), - text = stringResource(id = R.string.custom_token_subtitle), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) + if (model is AddCustomTokenUM.NetworkSelector && model.selectedNetwork != null) { + Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing12)) + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = TangemTheme.dimens.spacing16), + contentAlignment = Alignment.Center, + ) { + Text( + modifier = Modifier.fillMaxWidth(fraction = 0.7f), + text = stringResource(id = R.string.custom_token_subtitle), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } } } @@ -131,8 +161,18 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< override val values: Sequence get() = sequenceOf( PreviewAddCustomTokenComponent(), + PreviewAddCustomTokenComponent( + initialState = AddCustomTokenUM.NetworkSelector( + popBack = {}, + selectedNetwork = SelectedNetworkUM( + id = Network.ID(value = "0"), + name = "Ethereum", + ), + ), + ), PreviewAddCustomTokenComponent( initialState = AddCustomTokenUM.Form( + popBack = {}, selectedNetwork = SelectedNetworkUM( id = Network.ID(value = "1"), name = "Ethereum", diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt index 10e6f1f107..57abdb15b5 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenNetworkSelectorContent.kt @@ -16,41 +16,50 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent import com.tangem.features.managetokens.component.preview.PreviewCustomTokenNetworkSelectorComponent import com.tangem.features.managetokens.entity.CurrencyNetworkUM import com.tangem.features.managetokens.entity.CustomTokenNetworkSelectorUM +import com.tangem.features.managetokens.entity.SelectedNetworkUM import com.tangem.features.managetokens.impl.R internal fun LazyListScope.customTokenNetworkSelectorContent(model: CustomTokenNetworkSelectorUM) { val lastIndex = model.networks.lastIndex - item { - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size36) - .background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.bottomSheet, - ), - ) { - Text( + if (model.showTitle) { + item { + Box( modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing6, - ) - .padding(horizontal = TangemTheme.dimens.spacing12), - text = "Choose network", - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size36) + .background( + color = TangemTheme.colors.background.primary, + shape = TangemTheme.shapes.bottomSheet, + ), + ) { + Text( + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing6, + ) + .padding(horizontal = TangemTheme.dimens.spacing12), + text = stringResource(R.string.add_custom_token_choose_network), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + } } } @@ -62,13 +71,16 @@ internal fun LazyListScope.customTokenNetworkSelectorContent(model: CustomTokenN modifier = Modifier .fillMaxWidth() .clip( - shape = if (index == lastIndex) { - RoundedCornerShape( + shape = when { + !model.showTitle && index == 0 -> RoundedCornerShape( + topStart = TangemTheme.dimens.radius16, + topEnd = TangemTheme.dimens.radius16, + ) + index == lastIndex -> RoundedCornerShape( bottomStart = TangemTheme.dimens.radius16, bottomEnd = TangemTheme.dimens.radius16, ) - } else { - RectangleShape + else -> RectangleShape }, ) .background(color = TangemTheme.colors.background.primary) @@ -115,11 +127,32 @@ private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) @Composable @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview_CustomTokenNetworkSelectorContent() { +private fun Preview_CustomTokenNetworkSelectorContent( + @PreviewParameter(CustomTokenNetworkSelectorComponentPreviewProvider::class) + component: CustomTokenNetworkSelectorComponent, +) { TangemThemePreview { LazyColumn { - PreviewCustomTokenNetworkSelectorComponent().content(this) + component.content(this) } } } + +private class CustomTokenNetworkSelectorComponentPreviewProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + PreviewCustomTokenNetworkSelectorComponent(), + PreviewCustomTokenNetworkSelectorComponent( + params = CustomTokenNetworkSelectorComponent.Params( + userWalletId = UserWalletId(stringValue = "321"), + selectedNetwork = SelectedNetworkUM( + id = Network.ID(value = "0"), + name = "", + ), + onNetworkSelected = {}, + ), + ), + ) +} // endregion Preview \ No newline at end of file From 9e54099b63c41e0f5bca5a56333221535fb5a14c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Aug 2024 13:25:59 +0400 Subject: [PATCH 26/87] Updated on 2026-08-14 --- .../managetokens/model/ManageTokensModel.kt | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index b7527834c1..2361655244 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -7,9 +7,9 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.entity.CurrencyItemUM import com.tangem.features.managetokens.entity.CurrencyNetworkUM import com.tangem.features.managetokens.entity.ManageTokensUM @@ -109,14 +109,11 @@ internal class ManageTokensModel @Inject constructor( private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex -> CurrencyNetworkUM( - id = networkIndex.toString(), - model = BlockchainRowUM( - name = "NETWORK$networkIndex", - type = "N$networkIndex", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = networkIndex == 0, - isSelected = false, - ), + id = Network.ID(networkIndex.toString()), + name = "NETWORK$networkIndex", + type = "N$networkIndex", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = networkIndex == 0, isSelected = false, onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) }, ) @@ -151,14 +148,11 @@ internal class ManageTokensModel @Inject constructor( it.fastForEachIndexed { index, network -> if (index == networkIndex) { it[index] = network.copy( - model = network.model.copy( - iconResId = if (isSelected) { - R.drawable.img_eth_22 - } else { - R.drawable.ic_eth_16 - }, - isSelected = isSelected, - ), + iconResId = if (isSelected) { + R.drawable.img_eth_22 + } else { + R.drawable.ic_eth_16 + }, isSelected = isSelected, ) } From af657b67fc21a0cd0dd62134c653c60d061e99a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Aug 2024 15:08:44 +0300 Subject: [PATCH 27/87] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- .../src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index ba5cf565d8..b4ac0b266e 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-713" +tangemBlockchainSdk = "develop-720" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 0d35e3f2db..63f530a1d7 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -351,7 +351,5 @@ private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Nexa, Blockchain.NexaTestnet, - Blockchain.Mantle, - Blockchain.MantleTestnet, Blockchain.Filecoin, ) \ No newline at end of file From 9d5a56d97aee622ba8be75dc98ce03c312000ee2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Aug 2024 15:22:04 +0300 Subject: [PATCH 28/87] Updated on 2026-08-14 --- .../api/markets/TangemTechMarketsApi.kt | 2 +- .../response/TokenMarketListResponse.kt | 2 + .../com/tangem/pagination/BatchListSource.kt | 11 +- .../markets/DefaultMarketsTokenRepository.kt | 9 +- .../domain/markets/TokenMarketListConfig.kt | 1 - .../impl/DefaultMarketsTokenListComponent.kt | 13 +- .../tokenlist/impl/model/MarketsListModel.kt | 7 +- .../converters/MarketsTokenItemConverter.kt | 4 +- .../MarketsListBatchFlowManager.kt | 158 ++++++++++-------- .../statemanager/MarketsListUMStateManager.kt | 76 +++++++-- .../ui/components/MarketsListLazyColumn.kt | 17 +- .../impl/ui/entity/MarketsListItemUM.kt | 2 +- 12 files changed, 201 insertions(+), 101 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt index d09e293151..3ccbbf00b2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -16,8 +16,8 @@ interface TangemTechMarketsApi { @Query("offset") offset: Int, @Query("limit") limit: Int, @Query("order") order: String, - @Query("general_coins") generalCoins: Boolean, @Query("search") search: String?, + @Query("timestamp") timestamp: Long?, ): ApiResponse @GET("coins/{coin_id}") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt index b1e8675206..a0f287971f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -14,6 +14,8 @@ data class TokenMarketListResponse( val limit: Int, @Json(name = "offset") val offset: Int, + @Json(name = "timestamp") + val timestamp: Long? = null, ) { data class Token( @Json(name = "id") diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index 158de617e7..db1c27bbbc 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -114,6 +114,12 @@ private class DefaultBatchListSource loadMoreActionJob?.cancel() reloadActionJob?.cancel() stopAllUpdates() + + state.value = BatchListState( + data = emptyList(), + status = PaginationStatus.InitialLoading, + ) + reloadActionJob = scope.launchFetch { reloadTask(action) } @@ -221,11 +227,6 @@ private class DefaultBatchListSource } private suspend fun reloadTask(action: BatchAction.Reload) { - state.value = BatchListState( - data = emptyList(), - status = PaginationStatus.InitialLoading, - ) - val res = runCatching { batchFetcher.fetchFirst(action.requestParams) }.getOrElse { diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index ce15e2c6fe..b58bd811d3 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -10,6 +10,7 @@ import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.pagination.* import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong internal class DefaultMarketsTokenRepository( @@ -42,9 +43,9 @@ internal class DefaultMarketsTokenRepository( interval = request.params.priceChangeInterval.toRequestParam(), order = request.params.order.toRequestParam(), search = searchText, - generalCoins = request.params.showUnder100kMarketCapTokens.not(), offset = request.offset, limit = request.limit, + timestamp = if (isFirstBatchFetching) null else requestTimeStamp.get(), ).getOrThrow() } @@ -58,7 +59,7 @@ internal class DefaultMarketsTokenRepository( } if (isFirstBatchFetching) { - requestTimeStamp.set(0) // TODO when backend is ready + requestTimeStamp.set(res.timestamp ?: 0) } val last = res.tokens.size < request.limit @@ -82,10 +83,12 @@ internal class DefaultMarketsTokenRepository( marketsApi = marketsApi, ) + val atomicInteger = AtomicInteger(0) + return BatchListSource( fetchDispatcher = dispatcherProvider.io, context = batchingContext, - generateNewKey = { it.size }, + generateNewKey = { atomicInteger.getAndIncrement() }, batchFetcher = createTokenMarketsFetcher(firstBatchSize = firstBatchSize, nextBatchSize = nextBatchSize), updateFetcher = tokenMarketsUpdateFetcher, ).toBatchFlow() diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt index ad952091b6..c3633f3925 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt @@ -3,7 +3,6 @@ package com.tangem.domain.markets data class TokenMarketListConfig( val fiatPriceCurrency: String, val searchText: String?, - val showUnder100kMarketCapTokens: Boolean, val priceChangeInterval: Interval, val order: Order, ) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt index 5a73f13956..64c1f0f751 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt @@ -1,11 +1,9 @@ package com.tangem.features.markets.tokenlist.impl -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp +import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel @@ -40,6 +38,13 @@ class DefaultMarketsTokenListComponent @AssistedInject constructor( onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier, ) { + LifecycleStartEffect(Unit) { + model.isVisibleOnScreen.value = true + onStopOrDispose { + model.isVisibleOnScreen.value = false + } + } + val state by model.state.collectAsStateWithLifecycle() val bsState by bottomSheetState diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index b2aca92901..4e4d85ea7f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -82,6 +82,7 @@ internal class MarketsListModel @Inject constructor( val tokenSelected = _tokenSelected.asSharedFlow() val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED) + val isVisibleOnScreen = MutableStateFlow(false) val state = marketsListUMStateManager.state.asStateFlow() @@ -201,6 +202,7 @@ internal class MarketsListModel @Inject constructor( marketsListUMStateManager.searchQueryFlow .filter { it.isNotEmpty() } .debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS) + .distinctUntilChanged() .filter { activeListManager == searchMarketsListManager } .collectLatest { searchMarketsListManager.reload(searchText = it) @@ -233,7 +235,10 @@ internal class MarketsListModel @Inject constructor( delay(timeMillis) // Update quotes only when the container bottom sheet is in the expanded state containerBottomSheetState.first { it == BottomSheetState.EXPANDED } - activeListManager.updateQuotes() // TODO update a batch that is currently on screen + // and is visible on the screen + isVisibleOnScreen.first { it } + + activeListManager.updateQuotes() } }.saveIn(updateQuotesJob) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index f27ba0a768..f19fc26c78 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -33,7 +33,7 @@ internal class MarketsTokenItemConverter( trendPercentText = value.getTrendPercent(), trendType = value.getTrendType(), chardData = value.getChartData(), - showUnder100kMarketCap = value.isUnder100kMarketCap(), + isUnder100kMarketCap = value.isUnder100kMarketCap(), ) } @@ -147,7 +147,7 @@ internal class MarketsTokenItemConverter( } private fun TokenMarket.isUnder100kMarketCap(): Boolean { - return tokenQuotes.currentPrice.compareTo(decimal100k) == -1 + return marketCap?.let { it < decimal100k } ?: true } private companion object { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt index ba4b9aba33..d84184d89a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt @@ -9,15 +9,19 @@ import com.tangem.features.markets.tokenlist.impl.model.utils.logUpdateResults import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM.TrendInterval import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM -import com.tangem.pagination.* +import com.tangem.pagination.Batch +import com.tangem.pagination.BatchAction +import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.PaginationStatus import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch private const val LOG_EVENTS = true @@ -33,6 +37,7 @@ internal class MarketsListBatchFlowManager( private val dispatchers: CoroutineDispatcherProvider, ) { private val actionsFlow = MutableSharedFlow>() + private val updateStateJob = JobHolder() private val batchFlow = getMarketsTokenListFlowUseCase( batchingContext = TokenListBatchingContext( @@ -42,6 +47,9 @@ internal class MarketsListBatchFlowManager( batchFlowType = batchFlowType, ) + private val resultBatches = MutableStateFlow(ResultBatches()) + private val uiBatches = resultBatches.map { it.uiBatches } + val uiItems: StateFlow> get() = uiBatches .map { batches -> @@ -97,16 +105,20 @@ internal class MarketsListBatchFlowManager( initialValue = false, ) - private val uiBatches = MutableStateFlow>>>(emptyList()) - init { batchFlow.state .map { it.data } .distinctUntilChanged { a, b -> - a.size == b.size && a.map { it.data }.flatten() == b.map { it.data }.flatten() + a.size == b.size && + a.map { it.key } == b.map { it.key } && + a.map { it.data }.flatten() == b.map { it.data }.flatten() } - .onEachWithPrevious { prev, list -> - updateState(prev, list) + .onEach { + coroutineScope { + launch { + updateState(it) + }.saveIn(updateStateJob) + } } .flowOn(dispatchers.default) .launchIn(modelScope) @@ -127,58 +139,75 @@ internal class MarketsListBatchFlowManager( } } - private fun updateState( - previousList: List>>?, - list: List>>, - forceUpdate: Boolean = false, - ) = uiBatches.update { items -> - val converter = MarketsTokenItemConverter(currentTrendInterval(), appCurrency = currentAppCurrency()) + private suspend fun updateState(newList: List>>, forceUpdate: Boolean = false) = + withContext(dispatchers.default) { + resultBatches.update { resultBatches -> + val items = resultBatches.uiBatches + val previousList = resultBatches.processedItems - if (previousList == null || list.size < previousList.size || forceUpdate) { - list.map { - Batch( - key = it.key, - data = converter.convertList(it.data), + val converter = MarketsTokenItemConverter(currentTrendInterval(), appCurrency = currentAppCurrency()) + + if (newList.isEmpty()) { + return@update ResultBatches(processedItems = emptyList()) + } + + val isInitialLoading = + forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key + + val outItems = if (isInitialLoading) { + newList.map { + Batch( + key = it.key, + data = converter.convertList(it.data), + ) + } + } else { + previousList!! + if (previousList.size != newList.size) { + val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet()) + val newBatches = newList.filter { keysToAdd.contains(it.key) } + + items + newBatches.map { + Batch( + key = it.key, + data = converter.convertList(it.data), + ) + } + } else { + items.mapIndexed { batchIndex, batch -> + val prevBatch = previousList[batchIndex] + val newBatch = newList[batchIndex] + if (previousList == newBatch) return@mapIndexed batch + + Batch( + key = batch.key, + data = batch.data.mapIndexed { index, marketsListItemUM -> + val prevItem = prevBatch.data[index] + val newItem = newBatch.data[index] + + converter.update( + prevItem, + marketsListItemUM, + newItem, + ) + }, + ) + } + } + } + + currentCoroutineContext().ensureActive() + + ResultBatches( + uiBatches = outItems, + processedItems = newList, ) } - } else { - if (previousList.size != list.size) { - val keysToAdd = list.map { it.key }.subtract(previousList.map { it.key }.toSet()) - val newBatches = list.filter { keysToAdd.contains(it.key) } - - items + newBatches.map { - Batch( - key = it.key, - data = converter.convertList(it.data), - ) - } - } else { - items.mapIndexed { batchIndex, batch -> - val prevBatch = previousList[batchIndex] - val newBatch = list[batchIndex] - if (previousList == newBatch) return@mapIndexed batch - - Batch( - key = batch.key, - data = batch.data.mapIndexed { index, marketsListItemUM -> - val prevItem = prevBatch.data[index] - val newItem = newBatch.data[index] - - converter.update( - prevItem, - marketsListItemUM, - newItem, - ) - }, - ) - } - } } - } fun reload(searchText: String? = null) { modelScope.launch { - uiBatches.value = emptyList() + resultBatches.value = ResultBatches() actionsFlow.emit( BatchAction.Reload( requestParams = TokenMarketListConfig( @@ -188,7 +217,6 @@ internal class MarketsListBatchFlowManager( } else { searchText ?: currentSearchText() }, - showUnder100kMarketCapTokens = false, // TODO priceChangeInterval = currentTrendInterval().toBatchRequestInterval(), order = currentSortByType().toRequestOrder(), ), @@ -206,12 +234,14 @@ internal class MarketsListBatchFlowManager( fun updateUIWithSameState() { modelScope.launch(dispatchers.default) { val current = batchFlow.state.value.data - updateState(current, current, forceUpdate = true) - } + updateState(current, forceUpdate = true) + }.saveIn(updateStateJob) } fun loadCharts(batchKeys: Set, interval: TrendInterval) { - modelScope.launch(dispatchers.default) { + if (batchKeys.isEmpty()) return + + modelScope.launch { val currentData = batchFlow.state.value.data val alreadyLoadedChartsBatchKeys = currentData .filter { @@ -266,7 +296,7 @@ internal class MarketsListBatchFlowManager( } fun clearStateAndStopAllActions() { - uiBatches.value = emptyList() + resultBatches.value = ResultBatches() modelScope.launch { actionsFlow.emit(BatchAction.Reset) } @@ -303,12 +333,8 @@ internal class MarketsListBatchFlowManager( } } - private fun Flow.onEachWithPrevious(operation: suspend (prev: T?, value: T) -> Unit): Flow = flow { - var prev: T? = null - collect { value -> - operation(prev, value) - prev = value - emit(value) - } - } + private data class ResultBatches( + val uiBatches: List>> = emptyList(), + val processedItems: List>>? = null, + ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index 0e2d8c4ac0..fabc2afbfd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -13,6 +13,8 @@ import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* @Stable @@ -99,22 +101,74 @@ internal class MarketsListUMStateManager( it.copy(list = ListUM.Loading) } else -> { - it.copy( - list = ListUM.Content( - items = uiItems, - loadMore = onLoadMoreUiItems, - visibleIdsChanged = visibleItemsChanged, - showUnder100kTokens = true, - onShowTokensUnder100kClicked = { }, - triggerScrollReset = consumedEvent(), - onItemClick = onTokenClick, - ), - ) + it.updateItems(newItems = uiItems) } } } } + private fun MarketsListUM.updateItems(newItems: ImmutableList): MarketsListUM { + val currentState = this + val isNextPageInSearch = isInSearchState && (this.list as? ListUM.Content)?.showUnder100kTokens == true + var searchUiItemsCached: ImmutableList = persistentListOf() + + val items = when { + isInSearchState && isNextPageInSearch.not() -> { + searchUiItemsCached = newItems + val filtered = newItems.filter { item -> item.isUnder100kMarketCap.not() }.toImmutableList() + + if (filtered.size == newItems.size) { + return currentState.copy(list = generalContentState(newItems)) + } else { + filtered + } + } + else -> { + searchUiItemsCached = persistentListOf() + newItems + } + } + + return currentState.copy( + list = ListUM.Content( + items = items, + loadMore = onLoadMoreUiItems, + visibleIdsChanged = visibleItemsChanged, + showUnder100kTokens = isInSearchState.not() || isNextPageInSearch, + onShowTokensUnder100kClicked = { + if (searchUiItemsCached.isNotEmpty()) { + state.update { s -> + if (s.list is ListUM.Content) { + s.copy( + list = s.list.copy( + items = searchUiItemsCached, + showUnder100kTokens = true, + ), + ) + } else { + s + } + } + } + }, + triggerScrollReset = consumedEvent(), + onItemClick = onTokenClick, + ), + ) + } + + private fun generalContentState(newItems: ImmutableList): ListUM.Content { + return ListUM.Content( + items = newItems, + loadMore = onLoadMoreUiItems, + visibleIdsChanged = visibleItemsChanged, + showUnder100kTokens = true, + onShowTokensUnder100kClicked = {}, + triggerScrollReset = consumedEvent(), + onItemClick = onTokenClick, + ) + } + private fun state(): MarketsListUM = MarketsListUM( list = ListUM.Loading, searchBar = SearchBarUM( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index 8745966639..57aa98cc35 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -25,6 +25,7 @@ import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM import kotlinx.coroutines.launch private const val LOAD_NEXT_PAGE_ON_END_INDEX = 50 +private const val TOKEN_LAZY_LIST_ID_SEPARATOR = "***" @Composable @Suppress("LongMethod") @@ -89,7 +90,7 @@ internal fun MarketsListLazyColumn( is ListUM.Content -> { items( items = state.items, - key = { it.id + it.marketCap.toString() }, + key = { it.id + TOKEN_LAZY_LIST_ID_SEPARATOR + it.marketCap.toString() }, ) { item -> MarketsListItem( model = item, @@ -117,8 +118,11 @@ internal fun MarketsListLazyColumn( buffer = LOAD_NEXT_PAGE_ON_END_INDEX, onLoadMore = remember(state) { { - if (state is ListUM.Content) { + if (state is ListUM.Content && state.showUnder100kTokens) { state.loadMore() + true + } else { + false } } }, @@ -184,7 +188,9 @@ private fun SearchNothingFoundText(modifier: Modifier = Modifier) { private fun VisibleItemsTracker(listState: LazyListState, state: ListUM) { val visibleItems by remember { derivedStateOf { - listState.layoutInfo.visibleItemsInfo.mapNotNull { it.key as? String } + listState.layoutInfo.visibleItemsInfo.mapNotNull { + (it.key as? String)?.split(TOKEN_LAZY_LIST_ID_SEPARATOR)?.first() + } } } @@ -196,7 +202,7 @@ private fun VisibleItemsTracker(listState: LazyListState, state: ListUM) { } @Composable -fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Unit, buffer: Int = 2) { +fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buffer: Int = 2) { val loadMore by remember { derivedStateOf { val layoutInfo = listState.layoutInfo @@ -212,8 +218,7 @@ fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Unit, buffer LaunchedEffect(loadMore) { if (loadMore && !emitted) { - emitted = true - onLoadMore() + emitted = onLoadMore() } } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListItemUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListItemUM.kt index d96e5f8e21..3dedf931c4 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListItemUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListItemUM.kt @@ -17,7 +17,7 @@ data class MarketsListItemUM( val trendPercentText: String, val trendType: PriceChangeType, val chardData: MarketChartRawData?, - val showUnder100kMarketCap: Boolean = false, + val isUnder100kMarketCap: Boolean = false, ) { val chartType: MarketChartLook.Type = when (trendType) { PriceChangeType.UP, From beffbaa16dc037819989aee600cc4ac8013e5a43 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Aug 2024 16:02:33 +0300 Subject: [PATCH 29/87] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../datasource/config/ConfigManagerImpl.kt | 1 + .../datasource/config/models/JsonModels.kt | 1 + .../tangem/core/ui/extensions/BlockchainIcons.kt | 13 +++++++++---- core/ui/src/main/res/drawable/ic_filecoin_22.xml | 10 ++++++++++ .../ui/src/main/res/drawable/img_filecoin_22.xml | 16 ++++++++++++++++ gradle/dependencies.toml | 2 +- .../converters/BlockchainSDKConfigConverter.kt | 1 + 8 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_filecoin_22.xml create mode 100644 core/ui/src/main/res/drawable/img_filecoin_22.xml diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index faab90517c..cb7c080185 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit faab90517cc23bc700a0028146d84bec056f6d49 +Subproject commit cb7c080185a65b3b25cee30c331b696f784783f9 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 b2f7de050a..408493a3d1 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 @@ -155,6 +155,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { polygonZkEvm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC), base = GetBlockAccessToken(rest = accessTokens.base?.jsonRPC), blast = GetBlockAccessToken(jsonRpc = accessTokens.blast?.jsonRPC), + filecoin = GetBlockAccessToken(jsonRpc = accessTokens.filecoin?.jsonRPC), ) } } 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 47ed736e3d..7a65c4ed6c 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 @@ -77,6 +77,7 @@ data class GetBlockAccessTokens( @Json(name = "zksync") val zksync: GetBlockToken?, @Json(name = "base") val base: GetBlockToken?, @Json(name = "blast") val blast: GetBlockToken?, + @Json(name = "filecoin") val filecoin: GetBlockToken?, ) @JsonClass(generateAdapter = true) 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 1b7678e46d..a1613fcb79 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 @@ -3,7 +3,7 @@ package com.tangem.core.ui.extensions import androidx.annotation.DrawableRes import com.tangem.core.ui.R -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "LongMethod") @DrawableRes fun getActiveIconRes(blockchainId: String): Int { return when (blockchainId) { @@ -71,11 +71,12 @@ fun getActiveIconRes(blockchainId: String): Int { "koinos", "koinos/test" -> R.drawable.img_koinos_22 "bittensor" -> R.drawable.img_bittensor_22 "blast", "blast/test" -> R.drawable.img_blast_22 + "filecoin" -> R.drawable.img_filecoin_22 else -> R.drawable.ic_alert_24 } } -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "LongMethod") @DrawableRes fun getActiveIconResByNetworkId(networkId: String): Int { return when (networkId) { @@ -143,6 +144,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "koinos", "koinos/test" -> R.drawable.img_koinos_22 "bittensor" -> R.drawable.img_bittensor_22 "blast", "blast/test" -> R.drawable.img_blast_22 + "filecoin" -> R.drawable.img_filecoin_22 else -> R.drawable.ic_alert_24 } } @@ -212,11 +214,12 @@ fun getActiveIconResByCoinId(coinId: String): Int { "koinos", "koinos/test" -> R.drawable.img_koinos_22 "bittensor" -> R.drawable.img_bittensor_22 "blast", "blast/test" -> R.drawable.img_blast_22 + "filecoin" -> R.drawable.img_filecoin_22 else -> R.drawable.ic_alert_24 } } -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "LongMethod") @DrawableRes fun getGreyedOutIconRes(blockchainId: String): Int { return when (blockchainId) { @@ -284,11 +287,12 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "koinos", "koinos/test" -> R.drawable.ic_koinos_22 "bittensor" -> R.drawable.ic_bittensor_22 "blast", "blast/test" -> R.drawable.ic_blast_22 + "filecoin" -> R.drawable.ic_filecoin_22 else -> R.drawable.ic_alert_24 } } -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "LongMethod") @DrawableRes fun getGreyedOutIconResByNetworkId(networkId: String): Int { return when (networkId) { @@ -356,6 +360,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "koinos", "koinos/test" -> R.drawable.ic_koinos_22 "bittensor" -> R.drawable.ic_bittensor_22 "blast", "blast/test" -> R.drawable.ic_blast_22 + "filecoin" -> R.drawable.ic_filecoin_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_filecoin_22.xml b/core/ui/src/main/res/drawable/ic_filecoin_22.xml new file mode 100644 index 0000000000..3c71f2b663 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_filecoin_22.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_filecoin_22.xml b/core/ui/src/main/res/drawable/img_filecoin_22.xml new file mode 100644 index 0000000000..aca11bbf42 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_filecoin_22.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index b4ac0b266e..0e8525138d 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-720" +tangemBlockchainSdk = "develop-723" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt index 9767d3d290..6209d90560 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt @@ -86,6 +86,7 @@ internal object BlockchainSDKConfigConverter : Converter Date: Thu, 1 Aug 2024 13:33:30 +0500 Subject: [PATCH 30/87] Updated on 2026-08-14 --- .../common/feedback/FeedbackDataBuilder.kt | 6 - .../tangem/tap/di/domain/CardDomainModule.kt | 4 +- .../card/DefaultDerivationsRepository.kt | 30 +--- .../GetExtendedPublicKeyForCurrencyUseCase.kt | 130 ++++++++++++++++-- .../card/repository/DerivationsRepository.kt | 8 +- .../viewmodels/TokenDetailsViewModel.kt | 2 +- 6 files changed, 135 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt index e3add3c7cc..e9432205b9 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt @@ -31,12 +31,6 @@ class FeedbackDataBuilder( builder.appendDelimiter() builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName) builder.appendKeyValue("Derivation path", walletInfo.derivationPath) - - // enable later - // if (walletInfo.blockchain == Blockchain.Bitcoin) { - // builder.appendKeyValue("XPUB", infoHolder.extendedPublicKey) - // } - builder.appendKeyValue("Outputs count", walletInfo.outputsCount) if (walletInfo.tokens.isNotEmpty()) { 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 0f3fab437c..c74a09521c 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 @@ -6,6 +6,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase @@ -69,8 +70,9 @@ internal object CardDomainModule { @Singleton fun provideGetExtendedPublicKeyForCurrencyUseCase( derivationsRepository: DerivationsRepository, + walletManagersFacade: WalletManagersFacade, ): GetExtendedPublicKeyForCurrencyUseCase { - return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository) + return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository, walletManagersFacade) } @Provides 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 64e1b31472..5f4b695932 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 @@ -8,12 +8,10 @@ import com.tangem.common.doOnSuccess import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey 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.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap @@ -46,12 +44,16 @@ internal class DefaultDerivationsRepository( return } + derivePublicKeys(userWalletId = userWalletId, derivations = derivations) + } + + override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys { tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations) .doOnSuccess { response -> updatePublicKeys(userWalletId = userWalletId, keys = response.entries) .doOnSuccess { validateDerivations(scanResponse = it.scanResponse, derivations = derivations) - return + return response.entries } .doOnFailure { throw it } } @@ -60,28 +62,6 @@ 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 - } - } - /** * It throws an exception if any of the provided derivations are invalid * Validation for NonHardened moved to application layer, to avoid fails when derive multiple paths 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 index 4f107a0948..5990fe9c1a 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt @@ -1,30 +1,140 @@ package com.tangem.domain.card import arrow.core.Either +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.calculateRipemd160 +import com.tangem.common.extensions.calculateSha256 import com.tangem.crypto.NetworkType import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.operations.derivation.ExtendedPublicKeysMap /** * Derivates an exteneded public key (xpub) based on blockchain hardened derivation */ class GetExtendedPublicKeyForCurrencyUseCase( private val derivationsRepository: DerivationsRepository, + private val walletManagersFacade: WalletManagersFacade, ) { - suspend operator fun invoke( - userWalletId: UserWalletId, - derivation: Network.DerivationPath, - ): Either { + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either { return Either.catch { - val derivationPath = requireNotNull(derivation.value?.let { DerivationPath(it) }) { - error("Derivation is null") + val userWallet = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) + ?: error("Wallet not found") + + val blockchain = Blockchain.fromId(network.id.value) + val isSecp256k1Blockchain = Blockchain.secp256k1Blockchains(network.isTestnet).contains(blockchain) + + val hdKey = if (isSecp256k1Blockchain) { + userWallet.wallet.publicKey.derivationType?.hdKey ?: error("No derivation found") + } else { + error("No derivation found") } - val hardenedNodes = derivationPath.nodes.filter { it.isHardened } - val hardenedDerivation = DerivationPath(hardenedNodes) - derivationsRepository.deriveExtendedPublicKey(userWalletId, hardenedDerivation) - ?.serialize(NetworkType.Mainnet).orEmpty() + + var childKey = makeChildKey( + isBip44DerivationStyleXPUB = blockchain.isBip44DerivationStyleXPUB(), + extendedPublicKey = hdKey.extendedPublicKey, + derivationPath = hdKey.path, + ) + + var parentKey = Key( + derivationPath = childKey.derivationPath.dropLastNodes(1), + extendedPublicKey = null, + ) + + val pendingDerivations = getPendingDerivations(childKey, parentKey) + val derivedKeys = deriveKeys( + userWalletId = userWalletId, + seedKey = userWallet.wallet.publicKey.seedKey, + paths = pendingDerivations, + ) + + if (childKey.extendedPublicKey == null) { + childKey = childKey.copy( + extendedPublicKey = derivedKeys[childKey.derivationPath] ?: error("Failed to derive child key"), + ) + } + + if (parentKey.extendedPublicKey == null) { + parentKey = parentKey.copy( + extendedPublicKey = derivedKeys[parentKey.derivationPath] ?: error("Failed to derive parent key"), + ) + } + + makeExtendedKey(childKey, parentKey, network.isTestnet) } } + + private suspend fun deriveKeys( + userWalletId: UserWalletId, + seedKey: ByteArray, + paths: MutableList, + ): ExtendedPublicKeysMap { + val result = derivationsRepository.derivePublicKeys(userWalletId, mapOf(ByteArrayKey(seedKey) to paths)) + return result.getValue(ByteArrayKey(seedKey)) + } + + private fun makeExtendedKey(childKey: Key, parentKey: Key, isTestnet: Boolean): String { + val publicKey = childKey.extendedPublicKey?.publicKey ?: error("No public key found") + val chainCode = childKey.extendedPublicKey.chainCode + val lastChildNode = childKey.derivationPath.nodes.last() + val parentPublicKey = parentKey.extendedPublicKey?.publicKey + + val depth = childKey.derivationPath.nodes.size + val childNumber = lastChildNode.index + val parentFingerprint = parentPublicKey + ?.calculateSha256()?.calculateRipemd160() + ?.take(PARENT_FINGERPRINT_SIZE)?.toByteArray() + ?: error("No parent fingerprint found") + + val net = if (isTestnet) NetworkType.Testnet else NetworkType.Mainnet + return ExtendedPublicKey( + publicKey = publicKey, + chainCode = chainCode, + depth = depth, + parentFingerprint = parentFingerprint, + childNumber = childNumber, + ).serialize(net) + } + + private fun getPendingDerivations(childKey: Key, parentKey: Key): MutableList { + val pendingDerivations = mutableListOf() + + if (childKey.extendedPublicKey == null) { + pendingDerivations.add(childKey.derivationPath) + } + + if (parentKey.extendedPublicKey == null) { + pendingDerivations.add(parentKey.derivationPath) + } + + return pendingDerivations + } + + private fun makeChildKey( + isBip44DerivationStyleXPUB: Boolean, + extendedPublicKey: ExtendedPublicKey, + derivationPath: DerivationPath, + ): Key = if (isBip44DerivationStyleXPUB) { + Key(derivationPath.dropLastNodes(2), null) + } else { + Key(derivationPath, extendedPublicKey) + } + + private fun DerivationPath.dropLastNodes(count: Int): DerivationPath { + return DerivationPath(nodes.dropLast(count)) + } + + private data class Key( + val derivationPath: DerivationPath, + val extendedPublicKey: ExtendedPublicKey?, + ) + + private companion object { + const val PARENT_FINGERPRINT_SIZE = 4 + } } \ 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 209b68817a..d6b474839a 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,9 +1,10 @@ package com.tangem.domain.card.repository +import com.tangem.common.extensions.ByteArrayKey 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 +import com.tangem.operations.derivation.ExtendedPublicKeysMap interface DerivationsRepository { @@ -11,5 +12,8 @@ interface DerivationsRepository { suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) @Throws - suspend fun deriveExtendedPublicKey(userWalletId: UserWalletId, derivation: DerivationPath): ExtendedPublicKey? + suspend fun derivePublicKeys( + userWalletId: UserWalletId, + derivations: Map>, + ): Map } \ 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 0e56efee0d..a7877f7ceb 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 @@ -569,7 +569,7 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { val extendedKey = getExtendedPublicKeyForCurrencyUseCase( userWalletId, - cryptoCurrency.network.derivationPath, + cryptoCurrency.network, ).fold( ifLeft = { Timber.e(it.cause?.localizedMessage.orEmpty()) From 7208def757def60d464ce072e440fe3a5aa956e5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Aug 2024 15:46:37 +0300 Subject: [PATCH 31/87] Updated on 2026-08-14 --- .../data/feedback/converters/BlockchainInfoConverter.kt | 7 ++++++- .../java/com/tangem/domain/feedback/FeedbackDataBuilder.kt | 6 ++++-- .../com/tangem/domain/feedback/models/BlockchainInfo.kt | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) 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 index de7f9b6d04..686cec71dd 100644 --- 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 @@ -24,7 +24,12 @@ internal object BlockchainInfoConverter : Converter - BlockchainInfo.TokenInfo(id = token.id, name = token.name, contractAddress = token.contractAddress) + BlockchainInfo.TokenInfo( + id = token.id, + name = token.name, + contractAddress = token.contractAddress, + decimals = token.decimals.toString(), + ) }, ) } 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 a671bad033..8a5116c393 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 @@ -34,12 +34,14 @@ internal class FeedbackDataBuilder { 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("Token ID", token.id ?: "[custom token]") builder.appendKeyValue("Name", token.name) builder.appendKeyValue("Contract address", token.contractAddress) + builder.appendKeyValue("Decimals", token.decimals) + + builder.breakLine() } } 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 da0a0df2a5..a3665f64d2 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 @@ -24,5 +24,6 @@ data class BlockchainInfo( val id: String?, val name: String, val contractAddress: String, + val decimals: String, ) } \ No newline at end of file From 2f29f6d8d784b823ea04f5236af3961ae09053b9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 Aug 2024 14:41:54 +0300 Subject: [PATCH 32/87] Updated on 2026-08-14 --- .../com/tangem/data/feedback/converters/CardInfoConverter.kt | 4 ++++ .../java/com/tangem/domain/feedback/FeedbackDataBuilder.kt | 3 ++- .../main/java/com/tangem/domain/feedback/models/CardInfo.kt | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) 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 c4aafd2bf2..cb2081979e 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 @@ -20,6 +20,10 @@ internal object CardInfoConverter : Converter { CardInfo( userWalletId = createUserWalletId(scanResponse = value), cardId = card.cardId, + cardsCount = when (val status = value.card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount.toString() + else -> "0" + }, firmwareVersion = card.firmwareVersion.stringValue, cardBlockchain = walletData?.blockchain, signedHashesList = card.wallets.map { 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 8a5116c393..f69e364662 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 @@ -16,7 +16,8 @@ internal class FeedbackDataBuilder { 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("Linked cards count:", cardInfo.cardsCount) + builder.appendKeyValue("Has seed phrase:", cardInfo.isImported.toString()) builder.appendKeyValue("Card Blockchain", cardInfo.cardBlockchain) builder.appendSignedHashes(cardInfo.signedHashesList) } 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 2e7e710cc1..681cb957c0 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 @@ -6,6 +6,7 @@ data class CardInfo( val userWalletId: UserWalletId?, val cardId: String, val firmwareVersion: String, + val cardsCount: String, val cardBlockchain: String?, val signedHashesList: List, val isImported: Boolean, From 0f44f21d6fcb7fe845be1f42683695e105fec9ea Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 Aug 2024 10:52:45 +0300 Subject: [PATCH 33/87] Updated on 2026-08-14 --- .../tap/proxy/TransactionManagerImpl.kt | 33 ++++++++++++------- .../swap/domain/models/ui/SwapState.kt | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 23 ++++++++++++- gradle/dependencies.toml | 2 +- .../com/tangem/lib/crypto/models/ProxyFee.kt | 6 ++++ 5 files changed, 51 insertions(+), 14 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 c199b92e7b..bdd1dbb86a 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -101,18 +101,27 @@ class TransactionManagerImpl( // for not EVM blockchains set gasLimit ZERO for now when (fee.data) { is TransactionFee.Single -> { - val normalFee = (fee.data as TransactionFee.Single).normal - val singleFee = if (normalFee as? Fee.CardanoToken != null) { - ProxyFee.CardanoToken( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = normalFee.amount), - minAdaValue = normalFee.minAdaValue, - ) - } else { - ProxyFee.Common( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = normalFee.amount), - ) + val singleFee = when (val normalFee = (fee.data as TransactionFee.Single).normal) { + is Fee.CardanoToken -> { + ProxyFee.CardanoToken( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = normalFee.amount), + minAdaValue = normalFee.minAdaValue, + ) + } + is Fee.Filecoin -> { + ProxyFee.Filecoin( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = normalFee.amount), + gasPremium = normalFee.gasPremium, + ) + } + else -> { + ProxyFee.Common( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = normalFee.amount), + ) + } } ProxyFees.SingleFee(singleFee = singleFee) diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index f5fa03d4e2..c90f098662 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -105,6 +105,7 @@ data class TxFee( val decimals: Int, val cryptoSymbol: String, val feeType: FeeType, + val gasPremium: Long?, ) enum class FeeType { 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 18679abdb6..73dceaab1e 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 @@ -889,6 +889,21 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = fee.gasLimit.toLong(), ) } + blockchain == Blockchain.Filecoin -> { + val gasUnitPrice = fee.feeValue.divide( + BigDecimal(fee.gasLimit), + Blockchain.Filecoin.decimals(), + RoundingMode.HALF_UP, + ) + Fee.Filecoin( + amount = feeAmount, + gasUnitPrice = gasUnitPrice + .movePointRight(Blockchain.Filecoin.decimals()) + .toLong(), + gasLimit = fee.gasLimit.toLong(), + gasPremium = requireNotNull(fee.gasPremium), + ) + } else -> Fee.Common(feeAmount) } } @@ -1513,6 +1528,7 @@ internal class SwapInteractorImpl @Inject constructor( decimals = minFee.fee.decimals, cryptoSymbol = minFee.fee.currencySymbol, feeType = FeeType.NORMAL, + gasPremium = (minFee as? ProxyFee.Filecoin)?.gasPremium, ), priorityFee = TxFee( feeValue = priorityFeeValue, @@ -1522,6 +1538,7 @@ internal class SwapInteractorImpl @Inject constructor( decimals = normalFee.fee.decimals, cryptoSymbol = normalFee.fee.currencySymbol, feeType = FeeType.PRIORITY, + gasPremium = (normalFee as? ProxyFee.Filecoin)?.gasPremium, ), ) } @@ -1544,6 +1561,7 @@ internal class SwapInteractorImpl @Inject constructor( decimals = singleFee.fee.decimals, cryptoSymbol = singleFee.fee.currencySymbol, feeType = FeeType.NORMAL, + gasPremium = (singleFee as? ProxyFee.Filecoin)?.gasPremium, ), ) } @@ -1575,6 +1593,7 @@ internal class SwapInteractorImpl @Inject constructor( decimals = normalFee.amount.decimals, cryptoSymbol = normalFee.amount.currencySymbol, feeType = FeeType.NORMAL, + gasPremium = (normalFee as? Fee.Filecoin)?.gasPremium, ), priorityFee = TxFee( feeValue = feePriority, @@ -1584,6 +1603,7 @@ internal class SwapInteractorImpl @Inject constructor( decimals = priorityFee.amount.decimals, cryptoSymbol = priorityFee.amount.currencySymbol, feeType = FeeType.PRIORITY, + gasPremium = (priorityFee as? Fee.Filecoin)?.gasPremium, ), ) } @@ -1603,6 +1623,7 @@ internal class SwapInteractorImpl @Inject constructor( decimals = normal.amount.decimals, cryptoSymbol = normal.amount.currencySymbol, feeType = FeeType.NORMAL, + gasPremium = (normal as? Fee.Filecoin)?.gasPremium, ), ) } @@ -1638,6 +1659,7 @@ internal class SwapInteractorImpl @Inject constructor( is Fee.Ethereum -> gasLimit.toInt() is Fee.VeChain -> gasLimit.toInt() is Fee.Aptos -> gasLimit.toInt() + is Fee.Filecoin -> gasLimit.toInt() else -> 0 } } @@ -1856,7 +1878,6 @@ internal class SwapInteractorImpl @Inject constructor( normalFee = ProxyFee.Common( gasLimit = 1.toBigInteger(), fee = demoFee.copy(value = normalDemoFee), - ), priorityFee = ProxyFee.Common( gasLimit = 1.toBigInteger(), diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 0e8525138d..7485afe234 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-723" +tangemBlockchainSdk = "develop-724" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt index a4643460ea..77b655eb1f 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt @@ -18,4 +18,10 @@ sealed interface ProxyFee { override val fee: ProxyAmount, val minAdaValue: BigDecimal, ) : ProxyFee + + data class Filecoin( + override val gasLimit: BigInteger, + override val fee: ProxyAmount, + val gasPremium: Long, + ) : ProxyFee } \ No newline at end of file From 4f237f898bde07cd1b360d44def2267902eebd22 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 Aug 2024 12:59:41 +0500 Subject: [PATCH 34/87] Updated on 2026-08-14 --- .../tangem/tap/features/home/HomeFragment.kt | 2 +- .../tangem/tap/features/home/HomeViewModel.kt | 2 +- .../tangem/tap/routing/utils/ChildFactory.kt | 8 +- .../com/tangem/common/routing/AppRoute.kt | 6 +- .../component/ManageTokensComponent.kt | 3 +- .../preview/PreviewManageTokensComponent.kt | 18 ++-- .../entity/ManageTokensTopBarUM.kt | 23 +++++ .../managetokens/entity/ManageTokensUM.kt | 45 ++++++++-- .../managetokens/model/ManageTokensModel.kt | 83 ++++++++++++++++--- .../managetokens/ui/ManageTokensScreen.kt | 78 ++++++++++++----- .../router/DefaultWalletRouter.kt | 2 +- 11 files changed, 218 insertions(+), 52 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensTopBarUM.kt diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index a39f6b2278..508c8a2f4e 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -98,7 +98,7 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber { viewModel.onSearchClick() } else { Analytics.send(IntroductionProcess.ButtonTokensList()) - store.dispatchNavigationAction { push(AppRoute.ManageTokens) } + store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) } store.dispatch(TokensAction.SetArgs.ReadAccess) } }, diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt index f68dac30ca..c545f2f83b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt @@ -61,7 +61,7 @@ internal class HomeViewModel @Inject constructor( analyticsEventHandler.send(IntroductionProcess.ButtonTokensList()) store.dispatch(TokensAction.SetArgs.ReadAccess) - store.dispatchNavigationAction { push(AppRoute.ManageTokens) } + store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) } } private fun scanCard() { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index b6d85b29ba..af1bebc286 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -126,7 +126,13 @@ internal class ChildFactory @Inject constructor( if (manageTokensToggles.isFeatureEnabled) { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = ManageTokensComponent.Params(), + params = ManageTokensComponent.Params( + mode = if (route.readOnlyContent) { + ManageTokensComponent.Mode.READ_ONLY + } else { + ManageTokensComponent.Mode.MANAGE + }, + ), componentFactory = manageTokensComponentFactory, ) } else { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 8bb8395a7d..38bff0851c 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -189,7 +189,11 @@ sealed class AppRoute(val path: String) : Route { } @Serializable - data object ManageTokens : AppRoute(path = "/manage_tokens") + data class ManageTokens( + val readOnlyContent: Boolean, + ) : AppRoute(path = "/manage_tokens/$readOnlyContent"), RouteBundleParams { + override fun getBundle(): Bundle = bundle(serializer()) + } @Serializable data object AddCustomToken : AppRoute(path = "/add_custom_token") diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt index 921c452548..a38a96267c 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt @@ -5,7 +5,8 @@ import com.tangem.core.ui.decompose.ComposableContentComponent interface ManageTokensComponent : ComposableContentComponent { - class Params + data class Params(val mode: Mode) + enum class Mode { READ_ONLY, MANAGE, } interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index 6923cb7136..980afcb895 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -6,15 +6,14 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.entity.CurrencyItemUM -import com.tangem.features.managetokens.entity.CurrencyNetworkUM -import com.tangem.features.managetokens.entity.ManageTokensUM +import com.tangem.features.managetokens.entity.* import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.ManageTokensScreen import kotlinx.collections.immutable.mutate @@ -28,11 +27,18 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { private val changedItemsIds: MutableSet = mutableSetOf() private var items = initItems() - private val previewState = MutableStateFlow( - value = ManageTokensUM( + value = ManageTokensUM.ManageContent( popBack = {}, items = items, + topBar = ManageTokensTopBarUM.ManageContent( + title = resourceReference(id = R.string.main_manage_tokens), + onBackButtonClick = {}, + endButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_plus_24, + onIconClicked = {}, + ), + ), search = SearchBarUM( placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), query = "", @@ -41,8 +47,8 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { onActiveChange = ::toggleSearchBar, ), hasChanges = false, + isLoading = false, onSaveClick = {}, - onAddCustomToken = {}, ), ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensTopBarUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensTopBarUM.kt new file mode 100644 index 0000000000..b2f08a0ef0 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensTopBarUM.kt @@ -0,0 +1,23 @@ +package com.tangem.features.managetokens.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class ManageTokensTopBarUM { + + abstract val title: TextReference + abstract val onBackButtonClick: () -> Unit + + data class ReadContent( + override val title: TextReference, + override val onBackButtonClick: () -> Unit, + ) : ManageTokensTopBarUM() + + data class ManageContent( + override val title: TextReference, + override val onBackButtonClick: () -> Unit, + val endButton: TopAppBarButtonUM, + ) : ManageTokensTopBarUM() +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt index c8678bdd38..d53e7e5e4d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt @@ -5,11 +5,40 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM import kotlinx.collections.immutable.ImmutableList @Immutable -internal data class ManageTokensUM( - val popBack: () -> Unit, - val items: ImmutableList, - val search: SearchBarUM, - val hasChanges: Boolean, - val onAddCustomToken: () -> Unit, - val onSaveClick: () -> Unit, -) \ No newline at end of file +internal sealed class ManageTokensUM { + + abstract val popBack: () -> Unit + abstract val isLoading: Boolean + abstract val items: ImmutableList + abstract val topBar: ManageTokensTopBarUM + abstract val search: SearchBarUM + + data class ReadContent( + override val popBack: () -> Unit, + override val isLoading: Boolean, + override val items: ImmutableList, + override val topBar: ManageTokensTopBarUM, + override val search: SearchBarUM, + ) : ManageTokensUM() + + data class ManageContent( + override val popBack: () -> Unit, + override val isLoading: Boolean, + override val items: ImmutableList, + override val topBar: ManageTokensTopBarUM, + override val search: SearchBarUM, + val onSaveClick: () -> Unit, + val hasChanges: Boolean, + ) : ManageTokensUM() + + fun copySealed( + search: SearchBarUM = this.search, + items: ImmutableList = this.items, + hasChanges: Boolean = this is ManageContent && this.hasChanges, + ): ManageTokensUM { + return when (this) { + is ManageContent -> copy(search = search, items = items, hasChanges = hasChanges) + is ReadContent -> copy(search = search, items = items) + } + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 2361655244..6141538abd 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -4,15 +4,16 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.Network -import com.tangem.features.managetokens.entity.CurrencyItemUM -import com.tangem.features.managetokens.entity.CurrencyNetworkUM -import com.tangem.features.managetokens.entity.ManageTokensUM +import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.entity.* import com.tangem.features.managetokens.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.mutate @@ -24,17 +25,33 @@ import javax.inject.Inject @ComponentScoped internal class ManageTokensModel @Inject constructor( + paramsContainer: ParamsContainer, private val router: Router, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { + private val params: ManageTokensComponent.Params = paramsContainer.require() private val changedItemsIds: MutableSet = mutableSetOf() private var items = initItems() - val state: MutableStateFlow = MutableStateFlow( - value = ManageTokensUM( + val state: MutableStateFlow = MutableStateFlow(value = getInitialState(mode = params.mode)) + + private fun getInitialState(mode: ManageTokensComponent.Mode): ManageTokensUM { + return when (mode) { + ManageTokensComponent.Mode.READ_ONLY -> createReadContentModel() + ManageTokensComponent.Mode.MANAGE -> createManageContentModel() + } + } + + private fun createReadContentModel(): ManageTokensUM.ReadContent { + return ManageTokensUM.ReadContent( popBack = router::pop, - items = items, + isLoading = false, + items = initItems(), + topBar = ManageTokensTopBarUM.ReadContent( + title = resourceReference(R.string.common_search_tokens), + onBackButtonClick = router::pop, + ), search = SearchBarUM( placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), query = "", @@ -42,20 +59,60 @@ internal class ManageTokensModel @Inject constructor( isActive = false, onActiveChange = ::toggleSearchBar, ), + ) + } + + private fun createManageContentModel(): ManageTokensUM.ManageContent { + return ManageTokensUM.ManageContent( + popBack = router::pop, + isLoading = false, + items = initItems(), + topBar = ManageTokensTopBarUM.ManageContent( + title = resourceReference(id = R.string.main_manage_tokens), + onBackButtonClick = router::pop, + endButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_plus_24, + onIconClicked = ::onAddCustomToken, + ), + ), + search = SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "", + onQueryChange = ::searchCurrencies, + isActive = false, + onActiveChange = ::toggleSearchBar, + ), + onSaveClick = ::onSaveClick, hasChanges = false, - onSaveClick = {}, - onAddCustomToken = {}, - ), - ) + ) + } + + private fun onAddCustomToken() { + // TODO: [REDACTED_JIRA] + } + + private fun onSaveClick() { + // TODO: [REDACTED_JIRA] + } @Suppress("UnusedPrivateMember") private fun searchCurrencies(query: String) { // TODO: [REDACTED_JIRA] + val newItems = if (query.isBlank()) { + initItems() + } else { + state.value.items.filter { currency -> + currency.model.name.contains(query, ignoreCase = true) + }.toPersistentList() + } + state.update { state -> + state.copySealed(search = state.search.copy(query = query), items = newItems) + } } private fun toggleSearchBar(isActive: Boolean) { state.update { state -> - state.copy( + state.copySealed( search = state.search.copy(isActive = isActive), ) } @@ -135,7 +192,7 @@ internal class ManageTokensModel @Inject constructor( items = items.mutate { it[index] = updatedItem } - state.copy(items = items) + state.copySealed(items = items) } } @@ -177,7 +234,7 @@ internal class ManageTokensModel @Inject constructor( items = items.mutate { it[currencyIndex] = updatedItem } - state.copy( + state.copySealed( items = items, hasChanges = changedItemsIds.isNotEmpty(), ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index e21347f830..9f9584fac9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FabPosition import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold @@ -36,12 +37,14 @@ import com.tangem.core.ui.components.rows.ArrowRow import com.tangem.core.ui.components.rows.BlockchainRow import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent import com.tangem.features.managetokens.entity.CurrencyItemUM import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM +import com.tangem.features.managetokens.entity.ManageTokensTopBarUM import com.tangem.features.managetokens.entity.ManageTokensUM import com.tangem.features.managetokens.impl.R import kotlinx.collections.immutable.ImmutableList @@ -57,14 +60,9 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi modifier = modifier, containerColor = TangemTheme.colors.background.primary, topBar = { - TangemTopAppBar( + ManageTokensTopBar( modifier = Modifier.statusBarsPadding(), - title = stringResource(id = R.string.main_manage_tokens), - startButton = TopAppBarButtonUM.Back(state.popBack), - endButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_plus_24, - onIconClicked = state.onAddCustomToken, - ), + topBar = state.topBar, ) }, content = { innerPadding -> @@ -72,18 +70,36 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi modifier = Modifier .padding(innerPadding) .fillMaxSize(), - state = state, + search = state.search, + items = state.items, + isLoading = state.isLoading, + hasChanges = state is ManageTokensUM.ManageContent && state.hasChanges, ) }, floatingActionButtonPosition = FabPosition.Center, floatingActionButton = { - SaveChangesButton( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - isVisible = state.hasChanges, - onClick = state.onSaveClick, - ) + if (state is ManageTokensUM.ManageContent) { + SaveChangesButton( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + isVisible = state.hasChanges, + onClick = state.onSaveClick, + ) + } + }, + ) +} + +@Composable +private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM, modifier: Modifier = Modifier) { + TangemTopAppBar( + modifier = modifier, + title = topBar.title.resolveReference(), + startButton = TopAppBarButtonUM.Back(topBar.onBackButtonClick), + endButton = when (topBar) { + is ManageTokensTopBarUM.ManageContent -> topBar.endButton + is ManageTokensTopBarUM.ReadContent -> null }, ) } @@ -106,24 +122,48 @@ private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: } @Composable -private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { +private fun LoadingContent() { + Box( + modifier = Modifier + .fillMaxSize() + .background(color = TangemTheme.colors.background.primary), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator(color = TangemTheme.colors.icon.accent) + } +} + +@Composable +private fun Content( + search: SearchBarUM, + items: ImmutableList, + isLoading: Boolean, + hasChanges: Boolean, + modifier: Modifier = Modifier, +) { Box(modifier = modifier) { Currencies( modifier = Modifier.fillMaxSize(), - items = state.items, - search = state.search, + items = items, + search = search, ) AnimatedVisibility( modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth(), - visible = state.hasChanges, + visible = hasChanges, label = "bottom_fade_visibility", ) { BottomFade() } } + + Crossfade(targetState = isLoading, label = "ManageTokensLoadingContent") { + if (it) { + LoadingContent() + } + } } @OptIn(ExperimentalFoundationApi::class) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 2ee8c80a31..a70acf53cd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -140,7 +140,7 @@ internal class DefaultWalletRouter( } override fun openManageTokensScreen() { - router.push(AppRoute.ManageTokens) + router.push(AppRoute.ManageTokens(readOnlyContent = false)) } override fun openScanFailedDialog(onTryAgain: () -> Unit) { From 459db3d0dfee179ea7ff7c3a0f422953af657177 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 Aug 2024 15:42:58 +0300 Subject: [PATCH 35/87] Updated on 2026-08-14 --- .../tap/common/haptic/DefaultHapticManager.kt | 36 ----- .../haptic/DefaultVibratorHapticManager.kt | 38 ++++++ .../java/com/tangem/tap/di/HapticModule.kt | 15 ++- .../com/tangem/tap/di/UiDependenciesModule.kt | 9 +- .../tangem/common/ui/charts/MarketChart.kt | 12 +- .../java/com/tangem/core/ui/UiDependencies.kt | 4 +- .../core/ui/haptic/DefaultHapticManager.kt | 32 +++++ .../tangem/core/ui/haptic/HapticManager.kt | 11 +- .../core/ui/haptic/MockHapticManager.kt | 30 ----- .../core/ui/haptic/TangemHapticEffect.kt | 125 ++++++++++++++++++ .../core/ui/haptic/VibratorHapticManager.kt | 15 +++ .../com/tangem/core/ui/res/TangemTheme.kt | 12 +- .../tangem/core/ui/res/TangemThemePreview.kt | 3 - .../tangem/core/ui/screen/ComposeScreen.kt | 2 +- .../impl/ui/components/MarketsListItem.kt | 5 +- .../viewmodels/TokenDetailsViewModel.kt | 9 +- .../WalletCurrencyActionsClickIntents.kt | 7 +- 17 files changed, 263 insertions(+), 102 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/haptic/DefaultHapticManager.kt create mode 100644 app/src/main/java/com/tangem/tap/common/haptic/DefaultVibratorHapticManager.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/haptic/DefaultHapticManager.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/haptic/TangemHapticEffect.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/haptic/VibratorHapticManager.kt diff --git a/app/src/main/java/com/tangem/tap/common/haptic/DefaultHapticManager.kt b/app/src/main/java/com/tangem/tap/common/haptic/DefaultHapticManager.kt deleted file mode 100644 index 688dea7f67..0000000000 --- a/app/src/main/java/com/tangem/tap/common/haptic/DefaultHapticManager.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.tap.common.haptic - -import android.os.Build -import android.os.VibrationEffect -import android.os.Vibrator -import com.tangem.core.ui.haptic.HapticManager - -class DefaultHapticManager(private val vibrator: Vibrator) : HapticManager { - - override fun vibrateShort() { - vibrate(VIBRATION_SHORT_DURATION) - } - - override fun vibrateMeduim() { - vibrate(VIBRATION_MEDIUM_LOW_DURATION) - } - - override fun vibrateLong() { - vibrate(VIBRATION_MEDIUM_DURATION) - } - - private fun vibrate(durationMs: Long) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - vibrator.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE)) - } else { - vibrator.vibrate(durationMs) - } - } - - companion object { - const val VIBRATION_SHORT_DURATION = 50L - const val VIBRATION_MEDIUM_LOW_DURATION = 75L - const val VIBRATION_MEDIUM_DURATION = 100L - const val VIBRATION_LONG_DURATION = 200L - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/haptic/DefaultVibratorHapticManager.kt b/app/src/main/java/com/tangem/tap/common/haptic/DefaultVibratorHapticManager.kt new file mode 100644 index 0000000000..2bcae01131 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/haptic/DefaultVibratorHapticManager.kt @@ -0,0 +1,38 @@ +package com.tangem.tap.common.haptic + +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import androidx.annotation.ChecksSdkIntAtLeast +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.haptic.VibratorHapticManager + +internal class DefaultVibratorHapticManager( + private val vibrator: Vibrator, +) : VibratorHapticManager { + + private val isHapticEnabled = deviceSupportsVibrationEffects() + + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q) + private fun deviceSupportsVibrationEffects(): Boolean = when { + !vibrator.hasVibrator() -> false + + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> + vibrator.areAllEffectsSupported( + TangemHapticEffect.OneTime.DoubleClick.code, + TangemHapticEffect.OneTime.HeavyClick.code, + TangemHapticEffect.OneTime.Tick.code, + TangemHapticEffect.OneTime.Click.code, + ) == Vibrator.VIBRATION_EFFECT_SUPPORT_YES + + Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -> true + + else -> false + } + + override fun performOneTime(effect: TangemHapticEffect.OneTime) { + if (!isHapticEnabled) return + + vibrator.vibrate(VibrationEffect.createPredefined(effect.code)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/HapticModule.kt b/app/src/main/java/com/tangem/tap/di/HapticModule.kt index bf8439f09e..dc04a3ce72 100644 --- a/app/src/main/java/com/tangem/tap/di/HapticModule.kt +++ b/app/src/main/java/com/tangem/tap/di/HapticModule.kt @@ -4,9 +4,9 @@ import android.content.Context import android.os.Build import android.os.Vibrator import android.os.VibratorManager -import com.tangem.tap.common.haptic.DefaultHapticManager -import com.tangem.core.ui.haptic.HapticManager -import com.tangem.core.ui.haptic.MockHapticManager +import com.tangem.tap.common.haptic.DefaultVibratorHapticManager +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.haptic.VibratorHapticManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,7 +20,7 @@ class HapticModule { @Provides @Singleton - fun provideHapticManager(@ApplicationContext context: Context): HapticManager { + fun provideHapticManager(@ApplicationContext context: Context): VibratorHapticManager { val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager vibratorManager.defaultVibrator @@ -29,9 +29,12 @@ class HapticModule { } return if (vibrator.hasVibrator()) { - DefaultHapticManager(vibrator = vibrator) + DefaultVibratorHapticManager(vibrator = vibrator) } else { - MockHapticManager + // mock + object : VibratorHapticManager { + override fun performOneTime(effect: TangemHapticEffect.OneTime) = Unit + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt index 22a522e553..44631d4c05 100644 --- a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt @@ -2,7 +2,7 @@ package com.tangem.tap.di import androidx.compose.material3.SnackbarHostState import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.theme.AppThemeModeHolder import dagger.Module @@ -17,9 +17,12 @@ internal object UiDependenciesModule { @Provides @Singleton - fun provideUiDependencies(hapticManager: HapticManager, appThemeModeHolder: AppThemeModeHolder): UiDependencies { + fun provideUiDependencies( + vibratorHapticManager: VibratorHapticManager, + appThemeModeHolder: AppThemeModeHolder, + ): UiDependencies { return object : UiDependencies { - override val hapticManager = hapticManager + override val vibratorHapticManager = vibratorHapticManager override val appThemeModeHolder = appThemeModeHolder override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState() override val eventMessageHandler: EventMessageHandler = EventMessageHandler() diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt index 3dca540806..4e43d74718 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -11,11 +11,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFontFamilyResolver -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontSynthesis import androidx.compose.ui.text.font.FontWeight @@ -52,6 +50,8 @@ import com.tangem.common.ui.charts.layer.rememberTangemChartMarker import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider import com.tangem.common.ui.charts.state.* import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.DateTimeFormatters @@ -134,7 +134,8 @@ private fun rememberMarketVisibilityListener( canvasWidth: Int, state: MarketChartState, ): CartesianMarkerVisibilityListener { - val haptic = LocalHapticFeedback.current + val haptic = LocalHapticManager.current + return remember(state, canvasWidth) { val maxCanvasXFloat = canvasWidth.toFloat().takeIf { it != 0f } @@ -145,7 +146,7 @@ private fun rememberMarketVisibilityListener( state.markerFraction = maxCanvasXFloat?.let { xCanvas / it } state.markerVisibilityListener.onShown(marker, targets) - haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + haptic.perform(TangemHapticEffect.View.ContextClick) } override fun onHidden(marker: CartesianMarker) { @@ -158,7 +159,8 @@ private fun rememberMarketVisibilityListener( state.markerFraction = maxCanvasXFloat?.let { xCanvas / it } state.markerVisibilityListener.onUpdated(marker, targets) - haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + + haptic.perform(TangemHapticEffect.View.TextHandleMove) } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt index 49a90c5af6..0db95df8d1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt @@ -2,14 +2,14 @@ package com.tangem.core.ui import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Stable -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.theme.AppThemeModeHolder @Stable interface UiDependencies { - val hapticManager: HapticManager + val vibratorHapticManager: VibratorHapticManager val appThemeModeHolder: AppThemeModeHolder diff --git a/core/ui/src/main/java/com/tangem/core/ui/haptic/DefaultHapticManager.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/DefaultHapticManager.kt new file mode 100644 index 0000000000..5a34d92702 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/haptic/DefaultHapticManager.kt @@ -0,0 +1,32 @@ +package com.tangem.core.ui.haptic + +import android.view.View +import androidx.core.view.ViewCompat + +internal class DefaultHapticManager( + private val view: View, + private val vibratorHapticManager: VibratorHapticManager?, +) : HapticManager { + + override fun perform(effect: TangemHapticEffect) { + when (effect) { + is TangemHapticEffect.View -> { + effect.androidHapticFeedbackCode?.let { + ViewCompat.performHapticFeedback(view, it) + } + } + is TangemHapticEffect.OneTime -> { + if (vibratorHapticManager != null) { + vibratorHapticManager.performOneTime(effect) + } else { + when (effect) { + TangemHapticEffect.OneTime.Tick -> perform(TangemHapticEffect.View.SegmentTick) + TangemHapticEffect.OneTime.Click -> perform(TangemHapticEffect.View.ContextClick) + TangemHapticEffect.OneTime.DoubleClick -> perform(TangemHapticEffect.View.ContextClick) + TangemHapticEffect.OneTime.HeavyClick -> perform(TangemHapticEffect.View.LongPress) + } + } + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/haptic/HapticManager.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/HapticManager.kt index b93c08bd26..9b0d3cd124 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/haptic/HapticManager.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/haptic/HapticManager.kt @@ -2,12 +2,13 @@ package com.tangem.core.ui.haptic import androidx.compose.runtime.Stable +/** + * Haptic feedback. + * @see [TangemHapticEffect.OneTime] for one-time effects. + * @see [TangemHapticEffect.View] for view effects. + */ @Stable interface HapticManager { - fun vibrateShort() - - fun vibrateMeduim() - - fun vibrateLong() + fun perform(effect: TangemHapticEffect) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt deleted file mode 100644 index ad74ee1bd1..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.core.ui.haptic - -import androidx.compose.ui.hapticfeedback.HapticFeedback -import androidx.compose.ui.hapticfeedback.HapticFeedbackType - -@Suppress("FunctionName") -fun MockHapticManager(mockHapticFeedback: HapticFeedback? = null): HapticManager = - if (mockHapticFeedback == null) MockHapticManager else MockHapticManagerImpl(mockHapticFeedback) - -val MockHapticManager: HapticManager = MockHapticManagerImpl() - -private class MockHapticManagerImpl( - private val mockHapticFeedback: HapticFeedback? = null, -) : HapticManager { - - override fun vibrateShort() { - mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove) - /** Intentionally do nothing */ - } - - override fun vibrateMeduim() { - mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove) - /** Intentionally do nothing */ - } - - override fun vibrateLong() { - mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.LongPress) - /** Intentionally do nothing */ - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/haptic/TangemHapticEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/TangemHapticEffect.kt new file mode 100644 index 0000000000..4be47d804d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/haptic/TangemHapticEffect.kt @@ -0,0 +1,125 @@ +package com.tangem.core.ui.haptic + +import android.os.Build +import android.os.VibrationEffect +import androidx.annotation.RequiresApi + +sealed interface TangemHapticEffect { + + /** + * For cases when view could not be visible on screen (ex. Activity is not in foreground) + * or there is no view context (ex. background service, Model, ViewModel) + */ + enum class OneTime : TangemHapticEffect { + Click, + DoubleClick, + HeavyClick, + Tick, + ; + + val code: Int + @RequiresApi(Build.VERSION_CODES.Q) + get() = when (this) { + Click -> VibrationEffect.EFFECT_CLICK + DoubleClick -> VibrationEffect.EFFECT_DOUBLE_CLICK + HeavyClick -> VibrationEffect.EFFECT_HEAVY_CLICK + Tick -> VibrationEffect.EFFECT_TICK + } + } + + /** + * Preferred way to provide haptic feedback for UI components + * @see [androidx.core.view.HapticFeedbackConstantsCompat] + */ + @Suppress("MagicNumber") + enum class View(internal val androidHapticFeedbackCode: Int? = null) : TangemHapticEffect { + /** + * The user has performed a long press on an object that is resulting in an action being + * performed + */ + LongPress(0), + /** + * The user has pressed on a virtual on-screen key + */ + VirtualKey(1), + /** + * The user has pressed either an hour or minute tick of a Clock + */ + ClockTick(4), + /** + * The user has performed a context click on an object + */ + ContextClick(6), + /** + * The user has pressed a virtual or software keyboard key + */ + KeyboardPress(3), + /** + * The user has released a virtual keyboard key + */ + KeyboardRelease(7), + /** + * The user has released a virtual key + */ + VirtualKeyRelease(8), + /** + * The user has performed a selection/insertion handle move on text field + */ + TextHandleMove(9), + /** + * The user has started a gesture (e.g. on the soft keyboard) + */ + GestureStart(12), + /** + * The user has finished a gesture (e.g. on the soft keyboard) + */ + GestureEnd(13), + /** + * A haptic effect to signal the confirmation or successful completion of a user interaction + */ + Confirm(16), + /** + * A haptic effect to signal the rejection or failure of a user interaction + */ + Reject(17), + /** + * The user has toggled a switch or button into the on position + */ + ToggleOn(21), + /** + * The user has toggled a switch or button into the off position + */ + ToggleOff(22), + /** + * The user is executing a swipe/drag-style gesture, such as pull-to-refresh, where the + * gesture action is “eligible” at a certain threshold of movement, and can be cancelled by + * moving back past the threshold. This constant indicates that the user's motion has just + * passed the threshold for the action to be activated on release + */ + GestureThresholdActivate(23), + /** + * The user is executing a swipe/drag-style gesture, such as pull-to-refresh, where the + * gesture action is “eligible” at a certain threshold of movement, and can be cancelled by + * moving back past the threshold. This constant indicates that the user's motion has just + * re-crossed back "under" the threshold for the action to be activated, meaning the gesture is + * currently in a cancelled state + */ + GestureThresholdDeactivate(24), + /** + * The user has started a drag-and-drop gesture. The drag target has just been "picked up" + */ + DragStart(25), + /** + * The user is switching between a series of potential choices, for example items in a list + * or discrete points on a slider + */ + SegmentTick(26), + /** + * The user is switching between a series of many potential choices, for example minutes on a + * clock face, or individual percentages. This constant is expected to be very soft, so as + * not to be uncomfortable when performed a lot in quick succession. If the device can’t make + * a suitably soft vibration, then it may not make any vibration + */ + SegmentFrequentTick(27), + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/haptic/VibratorHapticManager.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/VibratorHapticManager.kt new file mode 100644 index 0000000000..f152a10aca --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/haptic/VibratorHapticManager.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.haptic + +import androidx.compose.runtime.Stable + +/** + * Haptic feedback + * For cases when view could not be visible on screen (ex. Activity is not in foreground) + * or there is no view context (ex. background service, Model, ViewModel) + * @see [HapticManager] for view effects. + */ +@Stable +interface VibratorHapticManager { + + fun performOneTime(effect: TangemHapticEffect.OneTime) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 65ffdd461b..999276faf5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -8,10 +8,12 @@ import androidx.compose.material.ProvideTextStyle import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalView import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.tangem.core.ui.components.TangemShimmer +import com.tangem.core.ui.haptic.DefaultHapticManager import com.tangem.core.ui.haptic.HapticManager -import com.tangem.core.ui.haptic.MockHapticManager +import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.windowsize.WindowSize import com.valentinilk.shimmer.Shimmer @@ -21,7 +23,7 @@ fun TangemTheme( windowSize: WindowSize, typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, - hapticManager: HapticManager = MockHapticManager, + vibratorHapticManager: VibratorHapticManager? = null, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, content: @Composable () -> Unit, ) { @@ -40,6 +42,12 @@ fun TangemTheme( ) } + val view = LocalView.current + + val hapticManager = remember(view) { + DefaultHapticManager(view = view, vibratorHapticManager = vibratorHapticManager) + } + MaterialTheme( colors = materialThemeColors(colors = themeColors, isDark = isDark), ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt index c3ed096dce..0347ca4893 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt @@ -6,8 +6,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf -import androidx.compose.ui.platform.LocalHapticFeedback -import com.tangem.core.ui.haptic.MockHapticManager import com.tangem.core.ui.windowsize.rememberWindowSizePreview @Composable @@ -29,7 +27,6 @@ fun TangemThemePreview( typography = typography, dimens = dimens, windowSize = rememberWindowSizePreview(maxWidth, maxHeight), - hapticManager = MockHapticManager(LocalHapticFeedback.current), content = content, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt index d6b1869ec5..5557002b53 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -61,7 +61,7 @@ internal fun ComposeScreen.createComposeView(context: Context, activity: Activit TangemTheme( isDark = shouldUseDarkTheme(appThemeMode), windowSize = windowSize, - hapticManager = uiDependencies.hapticManager, + vibratorHapticManager = uiDependencies.vibratorHapticManager, snackbarHostState = uiDependencies.globalSnackbarHostState, ) { ScreenContent(modifier = screenModifier) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt index e68c508ef5..14873a4e8d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt @@ -39,6 +39,7 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.marketprice.PriceChangeInPercent import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme @@ -97,14 +98,14 @@ fun MarketsListItem( .collect { val border = -actionWidthPx * SWIPE_THRESHOLD_PERCENT if (it < border && actionPerformed.not()) { - hapticManager.vibrateLong() + hapticManager.perform(TangemHapticEffect.View.GestureThresholdActivate) actionPerformed = true releasePerformed = false } if (it > border) { if (releasePerformed.not()) { - hapticManager.vibrateShort() + hapticManager.perform(TangemHapticEffect.View.GestureThresholdDeactivate) releasePerformed = true } actionPerformed = false 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 718cb21cf6..67e0caa285 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 @@ -19,7 +19,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -120,7 +121,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val associateAssetUseCase: AssociateAssetUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, - private val hapticManager: HapticManager, + private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, private val getUserWalletUseCase: GetUserWalletUseCase, tokenDetailsFeatureToggles: TokenDetailsFeatureToggles, @@ -580,7 +581,7 @@ internal class TokenDetailsViewModel @Inject constructor( ifRight = { it }, ) if (extendedKey.isNotBlank()) { - hapticManager.vibrateMeduim() + vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) clipboardManager.setText(text = extendedKey) internalUiState.value = stateFactory.getStateAndTriggerEvent( state = internalUiState.value, @@ -802,7 +803,7 @@ internal class TokenDetailsViewModel @Inject constructor( val addresses = networkAddress.availableAddresses.mapToAddressModels(cryptoCurrency).toImmutableList() val defaultAddress = addresses.firstOrNull()?.value ?: return null - hapticManager.vibrateMeduim() + vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) clipboardManager.setText(text = defaultAddress) analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) return resourceReference(R.string.wallet_notification_address_copied) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index bdf5dae4ba..9ec62dbe52 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -13,7 +13,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver @@ -91,7 +92,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, private val reduxStateHolder: ReduxStateHolder, - private val hapticManager: HapticManager, + private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, private val getYieldUseCase: GetYieldUseCase, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { @@ -192,7 +193,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( val addresses = networkAddress.availableAddresses.mapToAddressModels(cryptoCurrency).toImmutableList() val defaultAddress = addresses.firstOrNull()?.value ?: return null - hapticManager.vibrateMeduim() + vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) clipboardManager.setText(text = defaultAddress) analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) return resourceReference(R.string.wallet_notification_address_copied) From 4b21bf989e123a00c4c3d1a52b9b639b5f0f4346 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 Aug 2024 16:48:57 +0300 Subject: [PATCH 36/87] Updated on 2026-08-14 --- .../api/markets/TangemTechMarketsApi.kt | 2 +- ...Response.kt => TokenMarketInfoResponse.kt} | 113 ++++---- .../com/tangem/core/ui/components/Buttons.kt | 3 + .../com/tangem/core/ui/components/Shimmers.kt | 84 ++++++ .../block/information/InformationBlock.kt | 2 - .../information/InformationBlockContent.kt | 7 +- .../core/ui/components/text/TooltipText.kt | 13 +- .../tangem/core/ui/res/TangemAnimations.kt | 33 +++ .../com/tangem/core/ui/utils/PreviewUtils.kt | 55 ++++ .../markets/DefaultMarketsTokenRepository.kt | 9 + .../converters/TokenMarketInfoConverter.kt | 117 ++++++++ .../tangem/domain/markets/TokenMarketInfo.kt | 81 ++++++ .../markets/GetTokenMarketInfoUseCase.kt | 19 ++ .../repositories/MarketsTokenRepository.kt | 2 + features/markets/impl/build.gradle.kts | 1 + .../details/impl/ui/components/InfoPoint.kt | 145 ++++++++++ .../impl/ui/components/InsightsBlock.kt | 198 +++++++++++++ .../details/impl/ui/components/LinksBlock.kt | 221 +++++++++++++++ .../ui/components/MarketTokenDetailsChart.kt | 2 +- .../impl/ui/components/MetricsBlock.kt | 166 +++++++++++ .../ui/components/PricePerformanceBlock.kt | 260 ++++++++++++++++++ .../impl/ui/components/SecurityScoreBlock.kt | 203 ++++++++++++++ .../details/impl/ui/entity/InfoPointUM.kt | 11 + .../details/impl/ui/entity/InsightsUM.kt | 11 + .../markets/details/impl/ui/entity/LinksUM.kt | 22 ++ .../impl/ui/entity/MarketsTokenDetailsUM.kt | 16 +- .../details/impl/ui/entity/MetricsUM.kt | 9 + .../impl/ui/entity/PricePerformanceUM.kt | 18 ++ .../details/impl/ui/entity/SecurityScoreUM.kt | 11 + 29 files changed, 1759 insertions(+), 75 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/{TokenMarketDetailsResponse.kt => TokenMarketInfoResponse.kt} (52%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/PreviewUtils.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InfoPointUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InsightsUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/LinksUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MetricsUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/PricePerformanceUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/SecurityScoreUM.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt index 3ccbbf00b2..7362229f4a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -24,7 +24,7 @@ interface TangemTechMarketsApi { suspend fun getCoinMarketData( @Path("coin_id") coinId: String, @Query("currency") currency: String, - ): ApiResponse + ): ApiResponse @GET("coins/{coin_id}/history") suspend fun getCoinChart( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt similarity index 52% rename from core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index 227496a66e..13b5e0bdee 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -3,21 +3,19 @@ package com.tangem.datasource.api.markets.models.response import com.squareup.moshi.Json import java.math.BigDecimal -data class TokenMarketDetailsResponse( +data class TokenMarketInfoResponse( @Json(name = "id") val id: String, @Json(name = "name") val name: String, @Json(name = "symbol") val symbol: String, - @Json(name = "active") - val active: Boolean, @Json(name = "current_price") val currentPrice: BigDecimal, @Json(name = "price_change_percentage") - val priceChangePercentage: PriceChangePercentage, + val priceChangePercentage: PriceChangePercentage?, @Json(name = "networks") - val networks: List, + val networks: List?, @Json(name = "short_description") val shortDescription: String?, @Json(name = "full_description") @@ -25,27 +23,28 @@ data class TokenMarketDetailsResponse( @Json(name = "insights") val insights: List?, @Json(name = "metrics") - val metrics: Metrics, + val metrics: Metrics?, @Json(name = "links") - val links: Links, + val links: Links?, @Json(name = "price_performance") - val pricePerformance: PricePerformance, + val pricePerformance: PricePerformance?, ) { + data class PriceChangePercentage( @Json(name = "24h") - val h24: BigDecimal, + val day: Int?, @Json(name = "1w") - val week1: BigDecimal, + val week: Int?, @Json(name = "1m") - val month1: BigDecimal, + val month: Int?, @Json(name = "3m") - val month3: BigDecimal, + val threeMonths: Int?, @Json(name = "6m") - val month6: BigDecimal, + val sixMonths: Int?, @Json(name = "1y") - val year1: BigDecimal, + val year: Int?, @Json(name = "all_time") - val allTime: BigDecimal, + val allTime: Int?, ) data class Network( @@ -55,78 +54,78 @@ data class TokenMarketDetailsResponse( val exchangeable: Boolean, @Json(name = "contract_address") val contractAddress: String, - @Json(name = "decimalCount") + @Json(name = "decimal_count") val decimalCount: Int, ) data class Insight( @Json(name = "holders_change") - val holdersChange: Change, + val holdersChange: Change?, @Json(name = "liquidity_change") - val liquidityChange: Change, + val liquidityChange: Change?, @Json(name = "buy_pressure_change") - val buyPressureChange: Change, + val buyPressureChange: Change?, @Json(name = "experienced_buyer_change") - val experiencedBuyerChange: Change, - ) { - data class Change( - @Json(name = "1d") - val day1: Int, - @Json(name = "1w") - val week1: Int, - @Json(name = "1m") - val month1: Int, - ) - } + val experiencedBuyerChange: Change?, + ) + + data class Change( + @Json(name = "24h") + val day: Int?, + @Json(name = "1w") + val week: Int?, + @Json(name = "1m") + val month: Int?, + ) data class Metrics( @Json(name = "market_rating") - val marketRating: Int, + val marketRating: Int?, @Json(name = "circulating_supply") - val circulatingSupply: BigDecimal, + val circulatingSupply: BigDecimal?, @Json(name = "market_cap") - val marketCap: BigDecimal, + val marketCap: BigDecimal?, @Json(name = "volume_24h") - val volume24h: BigDecimal, + val volume24h: BigDecimal?, @Json(name = "total_supply") - val totalSupply: BigDecimal, + val totalSupply: BigDecimal?, @Json(name = "fully_diluted_valuation") - val fullyDilutedValuation: BigDecimal, + val fullyDilutedValuation: BigDecimal?, ) data class Links( @Json(name = "official_links") - val officialLinks: List = emptyList(), + val officialLinks: List?, @Json(name = "social") - val social: List = emptyList(), + val social: List?, @Json(name = "repository") - val repository: List = emptyList(), + val repository: List?, @Json(name = "blockchain_site") - val blockchainSite: List = emptyList(), + val blockchainSite: List?, ) data class Link( @Json(name = "title") - val title: String?, + val title: String, @Json(name = "id") - val id: String, + val id: String?, @Json(name = "link") - val url: String, + val link: String, ) data class PricePerformance( - @Json(name = "high_price") - val highPrice: Price, - @Json(name = "low_price") - val lowPrice: Price, - ) { - data class Price( - @Json(name = "24h") - val h24: BigDecimal, - @Json(name = "1m") - val month1: BigDecimal, - @Json(name = "all_time") - val allTime: BigDecimal, - ) - } + @Json(name = "24h") + val day: Range?, + @Json(name = "1m") + val month: Range?, + @Json(name = "all_time") + val allTime: Range?, + ) + + data class Range( + @Json(name = "low") + val low: Int?, + @Json(name = "high") + val high: Int?, + ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 7ef56f015e..9667a00860 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -10,6 +10,7 @@ import androidx.compose.material.ButtonColors import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.common.* @@ -26,6 +27,7 @@ fun TextButton( onClick: () -> Unit, modifier: Modifier = Modifier, colors: ButtonColors = TangemButtonsDefaults.defaultTextButtonColors, + textStyle: TextStyle = TangemTheme.typography.button, enabled: Boolean = true, ) { TangemButton( @@ -37,6 +39,7 @@ fun TextButton( showProgress = false, colors = colors, size = TangemButtonSize.Text, + textStyle = textStyle, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index 2e5b4ee1c2..5d09690136 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -9,14 +9,23 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.PrimarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.* import com.valentinilk.shimmer.* @@ -45,6 +54,70 @@ fun CircleShimmer(modifier: Modifier = Modifier) { ) } +/** + * Shimmer for text + * Height will be set automatically + * + * @param textSizeHeight if true, height will be set to font size height. + */ +@Composable +fun TextShimmer( + style: TextStyle, + modifier: Modifier = Modifier, + text: String = "A", + radius: Dp = TangemTheme.dimens.radius3, + textSizeHeight: Boolean = false, +) { + if (textSizeHeight) { + val lineHeight = with(LocalDensity.current) { style.lineHeight.toDp() } + + Box( + modifier = Modifier.requiredHeight(lineHeight), + contentAlignment = Alignment.CenterStart, + ) { + Text( + modifier = modifier + .clip(RoundedCornerShape(size = radius)) + .shimmer(LocalTangemShimmer.current), + text = text, + style = style.copy(lineHeight = style.fontSize), + maxLines = 1, + ) + } + } else { + Text( + modifier = modifier + .clip(RoundedCornerShape(size = radius)) + .shimmer(LocalTangemShimmer.current), + text = text, + style = style, + maxLines = 1, + ) + } +} + +/** + * Shimmer for SmallButton + * Height and min width will be set automatically + */ +@Composable +fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) { + PrimarySmallButton( + config = SmallButtonConfig( + text = stringReference("B"), + onClick = {}, + icon = if (withIcon) { + TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24) + } else { + TangemButtonIconPosition.None + }, + ), + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius16)) + .shimmer(LocalTangemShimmer.current), + ) +} + internal val TangemShimmer: Shimmer @Composable get() = rememberShimmer( @@ -106,6 +179,17 @@ private fun ShimmersPreview() { .height(TangemTheme.dimens.size24), ) CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42)) + TextShimmer( + style = TangemTheme.typography.body1, + modifier = Modifier.fillMaxWidth(fraction = 0.4f), + ) + TextShimmer( + style = TangemTheme.typography.body1, + textSizeHeight = true, + modifier = Modifier.fillMaxWidth(fraction = 0.4f), + ) + SmallButtonShimmer(withIcon = true) + SmallButtonShimmer(withIcon = false) } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt index 027109f071..900aafe401 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt @@ -97,7 +97,6 @@ private fun Preview_Grid() { }, content = { GridItems( - itemPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing4), items = persistentListOf( stringReference("Fist item"), stringReference("Second item"), @@ -105,7 +104,6 @@ private fun Preview_Grid() { itemContent = { PreviewItem(text = it) }, - horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) }, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt index 1a40b55863..423100d2e9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt @@ -44,7 +44,6 @@ inline fun InformationBlockContentScope.GridItems( items: ImmutableList, itemContent: @Composable BoxScope.(T) -> Unit, modifier: Modifier = Modifier, - itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0), verticalAlignment: Alignment.Vertical = Alignment.Top, horizontalArragement: Arrangement.Horizontal = Arrangement.Start, ) { @@ -59,7 +58,6 @@ inline fun InformationBlockContentScope.GridItems( Column( modifier = modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, ) { rowItems.fastForEach { row -> @@ -70,10 +68,7 @@ inline fun InformationBlockContentScope.GridItems( ) { row.fastForEach { item -> Box( - modifier = Modifier - .padding(itemPadding) - .weight(1f), - contentAlignment = Alignment.Center, + modifier = Modifier.weight(1f), ) { itemContent(item) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt index 110080210d..801d47263b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -28,7 +29,7 @@ fun TooltipText( text: TextReference, onInfoClick: () -> Unit, modifier: Modifier = Modifier, - useSmallerText: Boolean = false, + textStyle: TextStyle = TangemTheme.typography.caption2, ) { val interactionSource = remember { MutableInteractionSource() } @@ -45,18 +46,16 @@ fun TooltipText( Text( modifier = Modifier.weight(1f, fill = false), text = text.resolveReference(), - style = if (useSmallerText) { - TangemTheme.typography.caption2 - } else { - TangemTheme.typography.subtitle2 - }, + style = textStyle, color = TangemTheme.colors.text.tertiary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) IconButton( - modifier = Modifier.requiredSize(TangemTheme.dimens.size24), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing4) + .requiredSize(TangemTheme.dimens.size16), interactionSource = interactionSource, onClick = onInfoClick, ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt new file mode 100644 index 0000000000..b735f815a7 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt @@ -0,0 +1,33 @@ +package com.tangem.core.ui.res + +import androidx.compose.animation.* +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.runtime.* + +@Immutable +object TangemAnimations { + + val transitionSpecs = TransitionSpecs + + @Composable + @NonRestartableComposable + fun horizontalIndicatorAsState(targetFraction: Float): State { + return animateFloatAsState( + targetValue = targetFraction, + animationSpec = tween(durationMillis = 300), + label = "Indicator fraction", + ) + } + + @Immutable + object TransitionSpecs { + + @Stable + val textChange: ContentTransform = + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)) togetherWith + fadeOut(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/PreviewUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/PreviewUtils.kt new file mode 100644 index 0000000000..6cb1f784b9 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/PreviewUtils.kt @@ -0,0 +1,55 @@ +package com.tangem.core.ui.utils + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.coroutines.delay + +/** + * A container that shows a shimmer effect on top of the actual content. + * The shimmer effect is toggled every 2 seconds. + * Used for previewing components with shimmer effect and comparing their sizes with the actual content. + * + * If height is changing during the preview, it means that the actual content is not aligned with the shimmer effect. + * + * @param actualContent The actual content to be displayed. + * @param shimmerContent The shimmer effect to be displayed. + */ +@Composable +fun PreviewShimmerContainer(actualContent: @Composable () -> Unit, shimmerContent: @Composable () -> Unit) { + Column { + var height by remember { mutableIntStateOf(0) } + Row { + Text("height = $height") + } + SpacerH4() + + var shimmerVisible by remember { mutableStateOf(true) } + + TangemThemePreview { + Box( + Modifier.onGloballyPositioned { + height = it.size.height + }, + ) { + actualContent() + if (shimmerVisible) { + shimmerContent() + } + } + } + + LaunchedEffect(Unit) { + while (true) { + delay(timeMillis = 2000) + shimmerVisible = !shimmerVisible + } + } + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index b58bd811d3..6bd59980f7 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -107,4 +107,13 @@ internal class DefaultMarketsTokenRepository( return tokenChartConverter.convert(interval, response.getOrThrow()) } + + override suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String): TokenMarketInfo { + val response = marketsApi.getCoinMarketData( + currency = fiatCurrencyCode, + coinId = tokenId, + ) + + return TokenMarketInfoConverter().convert(response.getOrThrow()) + } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt new file mode 100644 index 0000000000..524d27debd --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -0,0 +1,117 @@ +package com.tangem.data.markets.converters + +import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.utils.converter.Converter + +internal class TokenMarketInfoConverter : Converter { + + override fun convert(value: TokenMarketInfoResponse): TokenMarketInfo { + return with(value) { + TokenMarketInfo( + id = id, + name = name, + symbol = symbol, + currentPrice = currentPrice, + priceChangePercentage = priceChangePercentage?.convert(), + networks = networks?.convert(), + shortDescription = shortDescription, + fullDescription = fullDescription, + insights = insights?.convert(), + metrics = metrics?.convert(), + links = links?.convert(), + pricePerformance = pricePerformance?.convert(), + ) + } + } + + private fun TokenMarketInfoResponse.PriceChangePercentage.convert(): TokenMarketInfo.PriceChangePercentage { + return TokenMarketInfo.PriceChangePercentage( + day = day, + week = week, + month = month, + threeMonths = threeMonths, + sixMonths = sixMonths, + year = year, + allTime = allTime, + ) + } + + @JvmName("convertNetwork") + private fun List.convert(): List { + return map { + TokenMarketInfo.Network( + networkId = it.networkId, + exchangeable = it.exchangeable, + contractAddress = it.contractAddress, + decimalCount = it.decimalCount, + ) + } + } + + @JvmName("convertInsight") + private fun List.convert(): List { + return map { + TokenMarketInfo.Insight( + holdersChange = it.holdersChange?.convert(), + liquidityChange = it.liquidityChange?.convert(), + buyPressureChange = it.buyPressureChange?.convert(), + experiencedBuyerChange = it.experiencedBuyerChange?.convert(), + ) + } + } + + private fun TokenMarketInfoResponse.Change.convert(): TokenMarketInfo.Change { + return TokenMarketInfo.Change( + day = day, + week = week, + month = month, + ) + } + + private fun TokenMarketInfoResponse.Metrics.convert(): TokenMarketInfo.Metrics { + return TokenMarketInfo.Metrics( + marketRating = marketRating, + circulatingSupply = circulatingSupply, + marketCap = marketCap, + volume24h = volume24h, + totalSupply = totalSupply, + fullyDilutedValuation = fullyDilutedValuation, + ) + } + + private fun TokenMarketInfoResponse.Links.convert(): TokenMarketInfo.Links { + return TokenMarketInfo.Links( + officialLinks = officialLinks?.convert(), + social = social?.convert(), + repository = repository?.convert(), + blockchainSite = blockchainSite?.convert(), + ) + } + + @JvmName("convertLink") + private fun List.convert(): List { + return map { + TokenMarketInfo.Link( + title = it.title, + id = it.id, + link = it.link, + ) + } + } + + private fun TokenMarketInfoResponse.PricePerformance.convert(): TokenMarketInfo.PricePerformance { + return TokenMarketInfo.PricePerformance( + day = day?.convert(), + month = month?.convert(), + allTime = allTime?.convert(), + ) + } + + private fun TokenMarketInfoResponse.Range.convert(): TokenMarketInfo.Range { + return TokenMarketInfo.Range( + low = low, + high = high, + ) + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt new file mode 100644 index 0000000000..1ad9daf777 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt @@ -0,0 +1,81 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenMarketInfo( + val id: String, + val name: String, + val symbol: String, + val currentPrice: BigDecimal, + val priceChangePercentage: PriceChangePercentage?, + val networks: List?, + val shortDescription: String?, + val fullDescription: String?, + val insights: List?, + val metrics: Metrics?, + val links: Links?, + val pricePerformance: PricePerformance?, +) { + data class PriceChangePercentage( + val day: Int?, + val week: Int?, + val month: Int?, + val threeMonths: Int?, + val sixMonths: Int?, + val year: Int?, + val allTime: Int?, + ) + + data class Network( + val networkId: String, + val exchangeable: Boolean, + val contractAddress: String, + val decimalCount: Int, + ) + + data class Insight( + val holdersChange: Change?, + val liquidityChange: Change?, + val buyPressureChange: Change?, + val experiencedBuyerChange: Change?, + ) + + data class Change( + val day: Int?, + val week: Int?, + val month: Int?, + ) + + data class Metrics( + val marketRating: Int?, + val circulatingSupply: BigDecimal?, + val marketCap: BigDecimal?, + val volume24h: BigDecimal?, + val totalSupply: BigDecimal?, + val fullyDilutedValuation: BigDecimal?, + ) + + data class Links( + val officialLinks: List?, + val social: List?, + val repository: List?, + val blockchainSite: List?, + ) + + data class Link( + val title: String, + val id: String?, + val link: String, + ) + + data class PricePerformance( + val day: Range?, + val month: Range?, + val allTime: Range?, + ) + + data class Range( + val low: Int?, + val high: Int?, + ) +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt new file mode 100644 index 0000000000..f6b2ecf317 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.markets + +import arrow.core.Either +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.repositories.MarketsTokenRepository + +class GetTokenMarketInfoUseCase( + private val marketsTokenRepository: MarketsTokenRepository, +) { + + suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either { + return Either.catch { + marketsTokenRepository.getTokenInfo( + fiatCurrencyCode = appCurrency.code, + tokenId = tokenId, + ) + }.mapLeft {} + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index 8b3ce43ce5..0fd36a20fa 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -11,4 +11,6 @@ interface MarketsTokenRepository { ): TokenListBatchFlow suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart + + suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String): TokenMarketInfo } \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index d5b82bb93d..f2becec495 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { /* Compose */ implementation(deps.compose.coil) implementation(deps.compose.foundation) + implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt new file mode 100644 index 0000000000..5be2fb74a5 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt @@ -0,0 +1,145 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.text.TooltipText +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemAnimations +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM + +@Composable +internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), + horizontalAlignment = Alignment.Start, + ) { + if (infoPointUM.onInfoClick != null) { + TooltipText( + text = infoPointUM.title, + onInfoClick = infoPointUM.onInfoClick, + textStyle = TangemTheme.typography.caption2, + ) + } else { + Text( + text = infoPointUM.title.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + AnimatedContent( + modifier = Modifier, + targetState = infoPointUM.value, + transitionSpec = { TangemAnimations.transitionSpecs.textChange }, + label = "insight block", + ) { + Text( + text = it, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +@Composable +internal fun InfoPointShimmer(modifier: Modifier = Modifier, withTooltip: Boolean = false) { + Column( + modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), + horizontalAlignment = Alignment.Start, + ) { + if (withTooltip) { + Box( + modifier = Modifier + .requiredHeight(TangemTheme.dimens.size16) + .fillMaxWidth(), + contentAlignment = Alignment.CenterStart, + ) { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.caption2, + textSizeHeight = false, + ) + } + } else { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.caption2, + textSizeHeight = true, + ) + } + TextShimmer( + modifier = Modifier.fillMaxWidth(fraction = 0.5f), + style = TangemTheme.typography.body1, + textSizeHeight = true, + ) + } +} + +@Preview +@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ContentPreview() { + TangemThemePreview { + Column( + modifier = Modifier + .width(150.dp) + .background(TangemTheme.colors.background.tertiary), + ) { + InfoPoint( + infoPointUM = InfoPointUM( + title = stringReference("Market Cap"), + value = "$1,000,000,000", + ), + ) + InfoPoint( + infoPointUM = InfoPointUM( + title = stringReference("Market Cap"), + value = "$1,000,000,000", + onInfoClick = { }, + ), + ) + } + } +} + +@Preview +@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewShimmer() { + TangemThemePreview { + PreviewShimmerContainer( + shimmerContent = { + Column( + modifier = Modifier + .width(150.dp) + .background(TangemTheme.colors.background.tertiary), + ) { + InfoPointShimmer(modifier = Modifier.fillMaxWidth()) + InfoPointShimmer( + modifier = Modifier.fillMaxWidth(), + withTooltip = true, + ) + } + }, + actualContent = { + ContentPreview() + }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt new file mode 100644 index 0000000000..a8cd4bf110 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt @@ -0,0 +1,198 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.block.information.GridItems +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM +import com.tangem.features.markets.details.impl.ui.entity.InsightsUM +import com.tangem.features.markets.details.impl.ui.getText +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) { + var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) } + + InformationBlock( + modifier = modifier, + title = { + Text( + modifier = Modifier, + text = stringResource(R.string.markets_token_details_insights), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + action = { + SegmentedButtons( + config = persistentListOf( + PriceChangeInterval.H24, + PriceChangeInterval.WEEK, + PriceChangeInterval.MONTH, + ), + initialSelectedItem = PriceChangeInterval.H24, + onClick = { currentInterval = it }, + ) { + Box( + Modifier + .fillMaxSize() + .align(Alignment.Center) + .padding(vertical = TangemTheme.dimens.spacing4), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = it.getText().resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + ) + } + } + }, + content = { + val infoPoints = when (currentInterval) { + PriceChangeInterval.H24 -> state.h24Info + PriceChangeInterval.WEEK -> state.weekInfo + PriceChangeInterval.MONTH -> state.monthInfo + else -> state.h24Info + } + + GridItems( + items = infoPoints, + itemContent = { + InfoPoint( + modifier = Modifier.align(Alignment.CenterStart), + infoPointUM = it, + ) + }, + ) + }, + ) +} + +@Composable +internal fun InsightsBlockPlaceholder(modifier: Modifier = Modifier) { + val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() } + val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() } + val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4 + + InformationBlock( + modifier = modifier, + title = { + RectangleShimmer( + modifier = Modifier + .height(headerHeight) + .fillMaxWidth(), + radius = TangemTheme.dimens.radius3, + ) + }, + content = { + GridItems( + items = List(size = 4) { it }.toImmutableList(), + horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + itemContent = { + InfoPointShimmer( + modifier = Modifier.fillMaxWidth(), + ) + }, + ) + }, + ) +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ContentPreview() { + TangemThemePreview { + InsightsBlock( + state = InsightsUM( + h24Info = persistentListOf( + InfoPointUM( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + value = "1 000 000 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_buy_pressure), + value = "1 000 000 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_holders), + value = "1 000 000 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_liquidity), + value = "1 000 000 000", + ), + ), + weekInfo = persistentListOf( + InfoPointUM( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + value = "1 000 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_buy_pressure), + value = "1 000 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_holders), + value = "1 000 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_liquidity), + value = "1 000 000", + ), + ), + monthInfo = persistentListOf( + InfoPointUM( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + value = "1 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_buy_pressure), + value = "1 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_holders), + value = "1 000", + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_liquidity), + value = "1 000", + ), + ), + ), + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewPlaceholder() { + TangemThemePreview { + PreviewShimmerContainer( + actualContent = { ContentPreview() }, + shimmerContent = { InsightsBlockPlaceholder() }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt new file mode 100644 index 0000000000..2d968b1327 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt @@ -0,0 +1,221 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.charts.downsample.fastForEach +import com.tangem.core.ui.components.SmallButtonShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.features.markets.details.impl.ui.entity.LinksUM +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + title = { + Text( + text = stringResource(id = R.string.markets_token_details_links), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + content = { + Column { + SubBlock( + title = stringResource(id = R.string.markets_token_details_official_links), + links = state.officialLinks, + onLinkClick = state.onLinkClick, + ) + SubBlock( + title = stringResource(id = R.string.markets_token_details_social), + links = state.social, + onLinkClick = state.onLinkClick, + ) + SubBlock( + title = stringResource(id = R.string.markets_token_details_repository), + links = state.repository, + onLinkClick = state.onLinkClick, + ) + SubBlock( + title = stringResource(id = R.string.markets_token_details_blockchain_site), + links = state.blockchainSite, + onLinkClick = state.onLinkClick, + ) + } + }, + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun SubBlock( + links: ImmutableList, + onLinkClick: (LinksUM.LinkUM) -> Unit, + modifier: Modifier = Modifier, + lastBlock: Boolean = false, + title: String = "Official links", +) { + if (links.isEmpty()) return + + DividerContainer( + modifier = modifier, + showDivider = !lastBlock, + ) { + Column( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Text( + text = title, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + links.fastForEach { + SecondarySmallButton( + config = SmallButtonConfig( + text = it.title, + onClick = { onLinkClick(it) }, + icon = TangemButtonIconPosition.Start(iconResId = it.iconRes), + ), + ) + } + } + } + } +} + +@Composable +fun LinksBlockPlaceholder(modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + title = { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.subtitle2, + ) + }, + content = { + Column { + SubBlockPlaceholder() + SubBlockPlaceholder() + SubBlockPlaceholder(lastBlock = true) + } + }, + ) +} + +@Composable +private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolean = false) { + DividerContainer( + modifier = modifier, + showDivider = !lastBlock, + ) { + Column( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + TextShimmer( + modifier = Modifier.width(78.dp), + style = TangemTheme.typography.caption2, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + repeat(times = 3) { + SmallButtonShimmer( + modifier = Modifier.weight(1f), + withIcon = true, + ) + } + } + } + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ContentPreview() { + TangemThemePreview { + LinksBlock( + state = LinksUM( + officialLinks = persistentListOf( + LinksUM.LinkUM( + title = stringReference("Website"), + iconRes = R.drawable.ic_plus_24, + url = "https://tangem.com", + ), + LinksUM.LinkUM( + title = stringReference("Website"), + iconRes = R.drawable.ic_plus_24, + url = "https://tangem.com", + ), + LinksUM.LinkUM( + title = stringReference("Website"), + iconRes = R.drawable.ic_plus_24, + url = "https://tangem.com", + ), + ), + social = persistentListOf( + LinksUM.LinkUM( + title = stringReference("Twitter"), + iconRes = R.drawable.ic_plus_24, + url = "https://tangem.com", + ), + LinksUM.LinkUM( + title = stringReference("Facebook"), + iconRes = R.drawable.ic_plus_24, + url = "https://tangem.com", + ), + ), + repository = persistentListOf( + LinksUM.LinkUM( + title = stringReference("Github"), + iconRes = R.drawable.ic_plus_24, + url = "https://tangem.com", + ), + ), + blockchainSite = persistentListOf(), + onLinkClick = {}, + ), + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PlaceholderPreview() { + TangemThemePreview { + PreviewShimmerContainer( + shimmerContent = { LinksBlockPlaceholder() }, + actualContent = { ContentPreview() }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt index d138a1c23d..ee9ce1d5b3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt @@ -18,7 +18,7 @@ import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData @Composable -fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) { +internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) { val growingColor = TangemTheme.colors.icon.accent val fallingColor = TangemTheme.colors.icon.warning diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt new file mode 100644 index 0000000000..20308e8e72 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt @@ -0,0 +1,166 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.TextButton +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.information.GridItems +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM +import com.tangem.features.markets.details.impl.ui.entity.MetricsUM +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +const val MAX_METRICS_COUNT = 6 + +@Composable +internal fun MetricsBlock(state: MetricsUM, modifier: Modifier = Modifier) { + var expanded by remember { mutableStateOf(false) } + + InformationBlock( + modifier = modifier, + title = { + Text( + text = stringResource(id = R.string.markets_token_details_metrics), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + action = { + if (state.metrics.size > MAX_METRICS_COUNT) { + ShowLessMoreButton(expanded = expanded, onClick = { expanded = !expanded }) + } + }, + content = { + val metrics = if (expanded) { + state.metrics + } else { + state.metrics.take(MAX_METRICS_COUNT).toImmutableList() + } + + GridItems( + items = metrics, + itemContent = { + InfoPoint(infoPointUM = it) + }, + ) + }, + ) +} + +// TODO make TextButton clickable area smaller and remove paddings for an action in InformationBlock +@Composable +private fun ShowLessMoreButton(expanded: Boolean, onClick: () -> Unit) { + // FIXME add string resources + val text = if (expanded) { + "See less" + } else { + "See more" + } + + TextButton( + text = text, + onClick = onClick, + colors = TangemButtonsDefaults.positiveButtonColors, + textStyle = TangemTheme.typography.body2, + ) +} + +@Composable +internal fun MetricsBlockPlaceholder(modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + title = { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + radius = TangemTheme.dimens.radius3, + style = TangemTheme.typography.subtitle2, + ) + }, + action = { + Box(Modifier) + }, + content = { + GridItems( + items = List(size = 6) { it }.toImmutableList(), + horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + itemContent = { + InfoPointShimmer( + modifier = Modifier.fillMaxWidth(), + withTooltip = true, + ) + }, + ) + }, + ) +} + +@Preview +@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun BlockPreview() { + TangemThemePreview { + MetricsBlock( + state = MetricsUM( + metrics = persistentListOf( + InfoPointUM( + title = resourceReference(R.string.markets_token_details_market_capitalization), + value = "1.2T", + onInfoClick = {}, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_market_rating), + value = "A", + onInfoClick = {}, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_trading_volume), + value = "1.2T", + onInfoClick = {}, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_fully_diluted_valuation), + value = "1.2T", + onInfoClick = {}, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_circulating_supply), + value = "1.2T", + onInfoClick = {}, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_total_supply), + value = "1.2T", + onInfoClick = {}, + ), + ), + ), + ) + } +} + +@Preview +@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewPlaceholder() { + TangemThemePreview { + PreviewShimmerContainer( + actualContent = { BlockPreview() }, + shimmerContent = { MetricsBlockPlaceholder() }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt new file mode 100644 index 0000000000..799200dc99 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -0,0 +1,260 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemAnimations +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.markets.details.impl.ui.entity.PricePerformanceUM +import com.tangem.features.markets.details.impl.ui.getText +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier = Modifier) { + var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) } + + InformationBlock( + modifier = modifier, + title = { + Text( + text = stringResource(id = R.string.markets_token_details_price_performance), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + action = { + SegmentedButtons( + config = persistentListOf( + PriceChangeInterval.H24, + PriceChangeInterval.MONTH, + PriceChangeInterval.ALL_TIME, + ), + initialSelectedItem = PriceChangeInterval.H24, + onClick = { currentInterval = it }, + ) { + Box( + Modifier + .fillMaxSize() + .align(Alignment.Center) + .padding(vertical = TangemTheme.dimens.spacing4), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = it.getText().resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + ) + } + } + }, + content = { + val value = when (currentInterval) { + PriceChangeInterval.H24 -> state.h24 + PriceChangeInterval.MONTH -> state.month + PriceChangeInterval.ALL_TIME -> state.all + else -> error("") + } + + Content( + modifier = Modifier.fillMaxWidth(), + state = value, + ) + }, + ) +} + +@Composable +private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifier) { + val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState( + targetFraction = state.indicatorFraction, + ) + + Column( + modifier = modifier + .padding(vertical = TangemTheme.dimens.spacing8), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.markets_token_details_low), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + SpacerW8() + Text( + text = stringResource(R.string.markets_token_details_high), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + LinearProgressIndicator( + modifier = Modifier + .height(TangemTheme.dimens.size6) + .fillMaxWidth(), + progress = { animatedIndicatorFraction }, + color = TangemTheme.colors.text.accent, + trackColor = TangemTheme.colors.background.tertiary, + strokeCap = StrokeCap.Round, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + AnimatedContent( + targetState = state.low, + transitionSpec = { TangemAnimations.transitionSpecs.textChange }, + label = "Low price", + ) { + Text( + text = it, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + SpacerW8() + AnimatedContent( + targetState = state.high, + transitionSpec = { TangemAnimations.transitionSpecs.textChange }, + label = "High price", + ) { + Text( + text = it, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.End, + ) + } + } + } +} + +@Composable +internal fun PricePerformanceBlockPlaceholder(modifier: Modifier = Modifier) { + val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() } + val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() } + val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4 + + InformationBlock( + modifier = modifier, + title = { + RectangleShimmer( + modifier = Modifier + .height(headerHeight) + .fillMaxWidth(), + radius = TangemTheme.dimens.radius3, + ) + }, + content = { + Column( + modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + TextShimmer( + modifier = Modifier.width(35.dp), + style = TangemTheme.typography.caption2, + ) + SpacerW8() + TextShimmer( + modifier = Modifier.width(35.dp), + style = TangemTheme.typography.caption2, + ) + } + RectangleShimmer( + modifier = Modifier + .height(TangemTheme.dimens.size6) + .fillMaxWidth(), + radius = 27.dp, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + TextShimmer( + modifier = Modifier.width(TangemTheme.dimens.size56), + style = TangemTheme.typography.body1, + ) + SpacerW8() + TextShimmer( + modifier = Modifier.width(TangemTheme.dimens.size56), + style = TangemTheme.typography.body1, + ) + } + } + }, + ) +} + +@Preview +@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ContentPreview() { + TangemThemePreview { + PricePerformanceBlock( + modifier = Modifier, + state = PricePerformanceUM( + h24 = PricePerformanceUM.Value( + low = "\$38,5K", + high = "\$58,5K", + indicatorFraction = 0.5f, + ), + month = PricePerformanceUM.Value( + low = "\$500,5K", + high = "\$5800,5K", + indicatorFraction = 0.8f, + ), + all = PricePerformanceUM.Value( + low = "\$58,52", + high = "\$580,5M", + indicatorFraction = 0.2f, + ), + ), + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PlaceholderPreview() { + TangemThemePreview { + PreviewShimmerContainer( + shimmerContent = { + PricePerformanceBlockPlaceholder() + }, + actualContent = { + ContentPreview() + }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt new file mode 100644 index 0000000000..319a08de34 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt @@ -0,0 +1,203 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.annotation.FloatRange +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.text.TooltipText +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.features.markets.details.impl.ui.entity.SecurityScoreUM +import com.tangem.features.markets.impl.R +import kotlin.math.round + +private const val STARS_COUNT = 5 + +@Composable +internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) { + val rounded = state.score.roundTo1decimal() + val percentage = rounded / STARS_COUNT + InformationBlock( + modifier = modifier, + title = { + Column( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + TooltipText( + text = resourceReference(R.string.markets_token_details_security_score), + onInfoClick = state.onInfoClick, + textStyle = TangemTheme.typography.subtitle2, + ) + + Text( + text = state.description, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + }, + action = { + Row( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = rounded.toString(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Stars(fraction = percentage) + } + }, + ) +} + +@Suppress("MagicNumber") +@Composable +private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) { + val grayColor = TangemTheme.colors.icon.inactive + + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(times = 5) { i -> + Box( + modifier = Modifier.size(TangemTheme.dimens.size16), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier + .requiredSize(13.dp) + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .drawWithCache { + onDrawWithContent { + val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0) + val starFractionFloat = starFraction + .toFloat() + .roundTo1decimal() + + drawContent() + drawRect( + color = grayColor, + topLeft = Offset(x = size.width * starFractionFloat, y = 0f), + size = Size(size.width * (1 - starFractionFloat), size.height), + blendMode = BlendMode.SrcIn, + ) + } + }, + imageVector = ImageVector.vectorResource(R.drawable.ic_star_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + } + } + } +} + +@Suppress("MagicNumber") +private fun Float.roundTo1decimal(): Float { + return round(this * 10) / 10 +} + +@Composable +internal fun SecurityScorePlaceHolder(modifier: Modifier = Modifier) { + InformationBlock( + modifier = modifier, + title = { + Column( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.subtitle2, + textSizeHeight = true, + ) + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + } + }, + action = { + Box( + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing24, + bottom = TangemTheme.dimens.spacing6, + ), + contentAlignment = Alignment.CenterEnd, + ) { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + RectangleShimmer( + modifier = Modifier + .height(TangemTheme.dimens.size16) + .fillMaxWidth(), + radius = TangemTheme.dimens.radius3, + ) + } + }, + ) +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ContentPreview() { + TangemThemePreview { + SecurityScoreBlock( + state = SecurityScoreUM( + score = 3.5f, + description = "Based on 3 ratings", + onInfoClick = {}, + ), + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewPlaceholder() { + TangemThemePreview { + PreviewShimmerContainer( + shimmerContent = { + SecurityScorePlaceHolder( + modifier = Modifier.fillMaxWidth(), + ) + }, + actualContent = { + ContentPreview() + }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InfoPointUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InfoPointUM.kt new file mode 100644 index 0000000000..44816c5ab1 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InfoPointUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal data class InfoPointUM( + val title: TextReference, + val value: String, + val onInfoClick: (() -> Unit)? = null, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InsightsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InsightsUM.kt new file mode 100644 index 0000000000..9c240b9214 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InsightsUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal data class InsightsUM( + val h24Info: PersistentList, + val weekInfo: PersistentList, + val monthInfo: PersistentList, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/LinksUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/LinksUM.kt new file mode 100644 index 0000000000..e22571424f --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/LinksUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal data class LinksUM( + val officialLinks: PersistentList, + val social: PersistentList, + val repository: PersistentList, + val blockchainSite: PersistentList, + val onLinkClick: (LinkUM) -> Unit, +) { + @Immutable + data class LinkUM( + @DrawableRes val iconRes: Int, + val title: TextReference, + val url: String, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt index 48941c6461..713d890bfb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt @@ -1,5 +1,6 @@ package com.tangem.features.markets.details.impl.ui.entity +import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.core.ui.components.marketprice.PriceChangeType @@ -7,7 +8,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.markets.PriceChangeInterval import java.math.BigDecimal -data class MarketsTokenDetailsUM( +@Immutable +internal data class MarketsTokenDetailsUM( val tokenName: String, val priceText: String, val iconUrl: String, @@ -17,8 +19,10 @@ data class MarketsTokenDetailsUM( val selectedInterval: PriceChangeInterval, val chartState: ChartState, val onSelectedIntervalChange: (PriceChangeInterval) -> Unit, + // val info : Information TODO [REDACTED_TASK_KEY] ) { + @Immutable data class ChartState( val status: Status, val dataProducer: MarketChartDataProducer, @@ -26,8 +30,18 @@ data class MarketsTokenDetailsUM( val onLoadRetryClick: () -> Unit, val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit, ) { + @Immutable enum class Status { LOADING, ERROR, DATA } } + + @Immutable + data class Information( + val insights: InsightsUM?, + val securityScore: SecurityScoreUM?, + val metrics: MetricsUM?, + val pricePerformance: PricePerformanceUM?, + val links: LinksUM?, + ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MetricsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MetricsUM.kt new file mode 100644 index 0000000000..14f5a9fb86 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MetricsUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal data class MetricsUM( + val metrics: PersistentList, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/PricePerformanceUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/PricePerformanceUM.kt new file mode 100644 index 0000000000..194354a363 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/PricePerformanceUM.kt @@ -0,0 +1,18 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import androidx.annotation.FloatRange +import androidx.compose.runtime.Immutable + +@Immutable +internal data class PricePerformanceUM( + val h24: Value, + val month: Value, + val all: Value, +) { + @Immutable + data class Value( + val low: String, + val high: String, + @FloatRange(from = 0.0, to = 1.0) val indicatorFraction: Float, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/SecurityScoreUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/SecurityScoreUM.kt new file mode 100644 index 0000000000..4afa252dba --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/SecurityScoreUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.markets.details.impl.ui.entity + +import androidx.annotation.FloatRange +import androidx.compose.runtime.Immutable + +@Immutable +internal data class SecurityScoreUM( + @FloatRange(from = 0.0, to = 5.0) val score: Float, + val description: String, + val onInfoClick: () -> Unit, +) \ No newline at end of file From f43f6299d1e97f9bf3250583e53b7ced21bf896f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 Aug 2024 19:02:08 +0500 Subject: [PATCH 37/87] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 3 - .../java/com/tangem/tap/TangemApplication.kt | 5 - .../tangem/tap/common/CurrencyConverter.kt | 36 -- .../tap/common/GlobalLayoutStateHandler.kt | 41 -- .../com/tangem/tap/common/KeyboardObserver.kt | 60 -- .../tap/common/analytics/events/Token.kt | 38 -- .../com/tangem/tap/common/entities/Button.kt | 16 - .../tangem/tap/common/extensions/Bitmap.kt | 11 - .../tangem/tap/common/extensions/Specific.kt | 17 - .../tangem/tap/common/extensions/WebView.kt | 8 - .../leapfrogWidget/TestLeapfrogFragment.kt | 72 --- .../recyclerView/SpaceItemDecoration.kt | 67 --- .../com/tangem/tap/common/redux/AppReducer.kt | 2 - .../com/tangem/tap/common/redux/AppState.kt | 4 - .../tap/common/redux/global/GlobalAction.kt | 3 - .../common/redux/global/GlobalMiddleware.kt | 13 - .../tap/common/snackBar/MaxAmountSnackbar.kt | 97 ---- .../common/text/DecimalDigitsInputFilter.kt | 60 -- .../com/tangem/tap/common/text/Truncate.kt | 130 ----- .../IndeterminateProgressButtonWidget.kt | 61 -- .../java/com/tangem/tap/domain/TapErrors.kt | 24 - .../tangem/tap/features/main/MainViewModel.kt | 9 - .../features/send/redux/SendScreenAction.kt | 215 ------- .../redux/middlewares/AddressMiddleware.kt | 225 -------- .../redux/middlewares/AddressValidator.kt | 70 --- .../redux/middlewares/AmountMiddleware.kt | 131 ----- .../redux/middlewares/RequestFeeMiddleware.kt | 132 ----- .../send/redux/middlewares/SendMiddleware.kt | 455 --------------- .../send/redux/reducers/AddressReducer.kt | 52 -- .../send/redux/reducers/AmountReducer.kt | 99 ---- .../send/redux/reducers/FeeReducer.kt | 122 ---- .../send/redux/reducers/ReceiptReducer.kt | 408 -------------- .../send/redux/reducers/SendScreenReducer.kt | 133 ----- .../reducers/TransactionExtrasReducer.kt | 201 ------- .../send/redux/states/AddressState.kt | 150 ----- .../features/send/redux/states/FeeState.kt | 43 -- .../send/redux/states/ReceiptState.kt | 69 --- .../features/send/redux/states/SendState.kt | 196 ------- .../features/send/ui/EditTextCustomPaste.kt | 29 - .../tap/features/send/ui/SendFragment.kt | 407 -------------- .../tap/features/send/ui/SendViewModel.kt | 140 ----- .../ui/adapters/WarningMessagesAdapter.kt | 122 ---- .../send/ui/dialogs/ChiaWarningDialog.kt | 32 -- .../send/ui/dialogs/KaspaWarningDialog.kt | 31 - .../send/ui/dialogs/RequestFeeErrorDialog.kt | 38 -- .../ui/dialogs/SendTransactionFailsDialog.kt | 65 --- .../send/ui/dialogs/TezosWarningDialog.kt | 29 - .../FragmentStateSubscriber.kt | 21 - .../stateSubscribers/SendStateSubscriber.kt | 530 ------------------ .../middlewares/TradeCryptoMiddleware.kt | 153 +---- .../tap/proxy/redux/DaggerGraphState.kt | 2 - app/src/main/res/layout/fragment_send.xml | 140 ----- .../main/res/layout/layout_send_address.xml | 329 ----------- .../main/res/layout/layout_send_amount.xml | 71 --- app/src/main/res/layout/layout_send_fee.xml | 93 --- .../main/res/layout/layout_send_receipt.xml | 87 --- .../res/layout/view_snackbar_max_amount.xml | 5 - .../view_snackbar_max_amount_content.xml | 22 - .../configs/feature_toggles_config.json | 4 - .../api/featuretoggles/SendFeatureToggles.kt | 13 - .../send/impl/di/SendFeatureTogglesModule.kt | 34 -- .../DefaultSendFeatureToggles.kt | 41 -- 62 files changed, 2 insertions(+), 5914 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/GlobalLayoutStateHandler.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/KeyboardObserver.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/entities/Button.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/extensions/Bitmap.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/extensions/WebView.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/leapfrogWidget/TestLeapfrogFragment.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/recyclerView/SpaceItemDecoration.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/snackBar/MaxAmountSnackbar.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/text/DecimalDigitsInputFilter.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/text/Truncate.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/toggleWidget/IndeterminateProgressButtonWidget.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressValidator.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/reducers/TransactionExtrasReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/states/AddressState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/states/ReceiptState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/ui/EditTextCustomPaste.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/ui/adapters/WarningMessagesAdapter.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/ui/dialogs/ChiaWarningDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/ui/dialogs/TezosWarningDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/FragmentStateSubscriber.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt delete mode 100644 app/src/main/res/layout/fragment_send.xml delete mode 100644 app/src/main/res/layout/layout_send_address.xml delete mode 100644 app/src/main/res/layout/layout_send_amount.xml delete mode 100644 app/src/main/res/layout/layout_send_fee.xml delete mode 100644 app/src/main/res/layout/layout_send_receipt.xml delete mode 100644 app/src/main/res/layout/view_snackbar_max_amount.xml delete mode 100644 app/src/main/res/layout/view_snackbar_max_amount_content.xml delete mode 100644 features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 967026bf4c..5c04f34268 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -32,7 +32,6 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles -import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles import com.tangem.tap.proxy.AppStateHolder @@ -82,8 +81,6 @@ interface ApplicationEntryPoint { fun getWalletsRepository(): WalletsRepository - fun getSendFeatureToggles(): SendFeatureToggles - fun getOneTimeEventFilter(): OneTimeEventFilter fun getGeneralUserWalletsListManager(): UserWalletsListManager diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index eaca995355..4e62a55ab3 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -47,7 +47,6 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles -import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler @@ -140,9 +139,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val walletsRepository: WalletsRepository get() = entryPoint.getWalletsRepository() - private val sendFeatureToggles: SendFeatureToggles - get() = entryPoint.getSendFeatureToggles() - private val oneTimeEventFilter: OneTimeEventFilter get() = entryPoint.getOneTimeEventFilter() @@ -271,7 +267,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { appThemeModeRepository = appThemeModeRepository, balanceHidingRepository = balanceHidingRepository, walletsRepository = walletsRepository, - sendFeatureToggles = sendFeatureToggles, generalUserWalletsListManager = generalUserWalletsListManager, wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase, saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, diff --git a/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt b/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt deleted file mode 100644 index d30c440661..0000000000 --- a/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.tap.common - -import com.tangem.common.extensions.isZero -import com.tangem.tap.common.extensions.scaleToFiat -import java.math.BigDecimal -import java.math.RoundingMode - -/** -[REDACTED_AUTHOR] - */ -class CurrencyConverter( - private val rateValue: BigDecimal, - private val decimals: Int, -) { - private val roundingMode = RoundingMode.HALF_UP - - fun toFiat(crypto: BigDecimal, fiatDecimals: Int = 2): BigDecimal { - return toFiatUnscaled(crypto).setScale(fiatDecimals, roundingMode) - } - - fun toFiatUnscaled(crypto: BigDecimal): BigDecimal { - return rateValue.multiply(crypto).setScale(decimals, roundingMode) - } - - fun toFiatWithPrecision(crypto: BigDecimal): BigDecimal { - return toFiatUnscaled(crypto).scaleToFiat(true) - } - - fun toCrypto(fiat: BigDecimal): BigDecimal { - if (fiat.isZero()) return fiat - - val fiatValue = fiat.setScale(rateValue.scale(), RoundingMode.UP) - val cryptoValue = fiatValue.divide(rateValue, RoundingMode.UP) - return cryptoValue.setScale(decimals, roundingMode) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/GlobalLayoutStateHandler.kt b/app/src/main/java/com/tangem/tap/common/GlobalLayoutStateHandler.kt deleted file mode 100644 index 0a51404cf9..0000000000 --- a/app/src/main/java/com/tangem/tap/common/GlobalLayoutStateHandler.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.tap.common - -import android.view.View -import android.view.ViewTreeObserver -import timber.log.Timber - -/** -[REDACTED_AUTHOR] - */ -class GlobalLayoutStateHandler( - private val view: T, - attachImmediately: Boolean = true, -) : ViewTreeObserver.OnGlobalLayoutListener { - - var onStateChanged: ((T) -> Unit)? = null - - private var isAttached: Boolean = false - - init { - if (attachImmediately) attach() - } - - private fun attach() { - if (isAttached) { - Timber.d("Already attached") - return - } - - isAttached = true - view.viewTreeObserver.addOnGlobalLayoutListener(this) - } - - fun detach() { - view.viewTreeObserver.removeOnGlobalLayoutListener(this) - isAttached = false - } - - override fun onGlobalLayout() { - onStateChanged?.invoke(view) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/KeyboardObserver.kt b/app/src/main/java/com/tangem/tap/common/KeyboardObserver.kt deleted file mode 100644 index 47f579ca1b..0000000000 --- a/app/src/main/java/com/tangem/tap/common/KeyboardObserver.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.tap.common - -import android.app.Activity -import android.graphics.Rect -import android.util.DisplayMetrics -import android.view.ViewTreeObserver.OnGlobalLayoutListener -import kotlin.math.absoluteValue - -class KeyboardObserver(activity: Activity) { - - private val decorView = activity.window.decorView - private val windowManager = activity.windowManager - private val originalWindowHeight: Int = getWindowHeight() - private val onGlobalLayoutListener: OnGlobalLayoutListener = OnGlobalLayoutListener { onGlobalLayout() } - - private var onKeyboardListener: ((Boolean) -> Unit)? = null - private var lastIsShow = false - private var lastWindowHeight = getWindowHeight() - - fun registerListener(listener: (Boolean) -> Unit) { - decorView.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener) - onKeyboardListener = listener - } - - fun unregisterListener() { - decorView.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener) - onKeyboardListener = null - } - - private fun getWindowHeight() = Rect().apply { decorView.getWindowVisibleDisplayFrame(this) }.bottom - - private fun onGlobalLayout() { - val currentWindowHeight = getWindowHeight() - if (isSoftKeyChanged()) { - lastWindowHeight = currentWindowHeight - return - } - - lastWindowHeight = currentWindowHeight - val isShow = originalWindowHeight != currentWindowHeight - if (lastIsShow == isShow) return - - lastIsShow = isShow - onKeyboardListener?.invoke(isShow) - } - - private fun isSoftKeyChanged() = (lastWindowHeight - getWindowHeight()).absoluteValue == getSoftKeyButtonHeight() - - private fun getSoftKeyButtonHeight(): Int { - val applicationDisplayHeight = DisplayMetrics().apply { - windowManager.defaultDisplay.getMetrics(this) - }.heightPixels - - val realDisplayHeight = DisplayMetrics().apply { - windowManager.defaultDisplay.getRealMetrics(this) - }.heightPixels - - return realDisplayHeight - applicationDisplayHeight - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt index 3b5ae220eb..56c286492d 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt @@ -22,44 +22,6 @@ sealed class Token( class ButtonShareAddress : Receive("Button - Share Address") } - sealed class Send( - event: String, - params: Map = mapOf(), - error: Throwable? = null, - ) : Token("Token / Send", event, params, error) { - - class ScreenOpened : Send(event = "Send Screen Opened") - class ButtonPaste : Send(event = "Button - Paste") - class ButtonQRCode : Send(event = "Button - QR Code") - class ButtonSwapCurrency : Send(event = "Button - Swap Currency") - - class AddressEntered(sourceType: SourceType, validationResult: ValidationResult) : Send( - event = "Address Entered", - params = mapOf( - "Source" to sourceType.name, - "Validation" to validationResult.name, - ), - ) { - enum class SourceType { - QRCode, PasteButton, PastePopup - } - - enum class ValidationResult { - Success, Fail - } - } - - class SelectedCurrency(currency: CurrencyType) : Send( - event = "Selected Currency", - params = mapOf("Type" to currency.value), - ) { - - enum class CurrencyType(val value: String) { - Token(value = "Token"), AppCurrency(value = "App Currency") - } - } - } - sealed class Topup( event: String, params: Map = mapOf(), diff --git a/app/src/main/java/com/tangem/tap/common/entities/Button.kt b/app/src/main/java/com/tangem/tap/common/entities/Button.kt deleted file mode 100644 index b0aca7f994..0000000000 --- a/app/src/main/java/com/tangem/tap/common/entities/Button.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.tap.common.entities - -import com.tangem.tap.features.send.redux.states.ButtonState - -open class Button(val enabled: Boolean) - -open class IndeterminateProgressButton( - val state: ButtonState, -) : Button(state != ButtonState.DISABLED) { - - val progressState: ProgressState - get() = when (state) { - ButtonState.PROGRESS -> ProgressState.Loading - else -> ProgressState.Done - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Bitmap.kt b/app/src/main/java/com/tangem/tap/common/extensions/Bitmap.kt deleted file mode 100644 index 76eea0c892..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/Bitmap.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.tap.common.extensions - -import android.graphics.Bitmap -import java.io.ByteArrayOutputStream - -@Suppress("MagicNumber") -fun Bitmap.toByteArray(): ByteArray { - val stream = ByteArrayOutputStream() - this.compress(Bitmap.CompressFormat.JPEG, 20, stream) - return stream.toByteArray() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt index a2206cb3bc..773bca1bd5 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt @@ -1,6 +1,5 @@ package com.tangem.tap.common.extensions -import com.tangem.common.extensions.isZero import java.math.BigDecimal import java.math.RoundingMode import java.text.DecimalFormat @@ -25,20 +24,4 @@ fun BigDecimal.toFormattedString( fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString() -// 0.00 -> 0.00 -// 0.00002345 -> 0.00002 -// 1.00002345 -> 1.00 -// 1.45002345 -> 1.45 -fun BigDecimal.scaleToFiat(applyPrecision: Boolean = false): BigDecimal { - if (this.isZero()) return this - - val scaledFiat = this.setScale(2, RoundingMode.DOWN) - return if (scaledFiat.isZero() && applyPrecision) this.setPrecision(1) else scaledFiat -} - -fun BigDecimal.setPrecision(precision: Int, roundingMode: RoundingMode = RoundingMode.DOWN): BigDecimal { - if (precision == precision() || scale() <= precision) return this - return this.setScale(scale() - precision() + precision, roundingMode) -} - fun BigDecimal.isPositive(): Boolean = this.signum() == 1 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WebView.kt b/app/src/main/java/com/tangem/tap/common/extensions/WebView.kt deleted file mode 100644 index 1afe53b3ba..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/WebView.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.tap.common.extensions - -import android.webkit.WebView - -fun WebView.stop() { - stopLoading() - pauseTimers() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/leapfrogWidget/TestLeapfrogFragment.kt b/app/src/main/java/com/tangem/tap/common/leapfrogWidget/TestLeapfrogFragment.kt deleted file mode 100644 index 8253e64b37..0000000000 --- a/app/src/main/java/com/tangem/tap/common/leapfrogWidget/TestLeapfrogFragment.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.tangem.tap.common.leapfrogWidget - -import android.os.Bundle -import android.view.View -import android.widget.FrameLayout -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater -import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget -import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidgetState -import com.tangem.tap.domain.twins.TwinsCardWidget -import com.tangem.wallet.R -import com.tangem.wallet.databinding.TestLeapfrogFragmentBinding - -class TestLeapfrogFragment : Fragment(R.layout.test_leapfrog_fragment) { - - private lateinit var twinsCardWidget: TwinsCardWidget - private val binding: TestLeapfrogFragmentBinding by viewBinding(TestLeapfrogFragmentBinding::bind) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val inflater = TransitionInflater.from(requireContext()) - exitTransition = inflater.inflateTransition(R.transition.fade) - } - - @Suppress("MagicNumber") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val leapfrogContainer: FrameLayout = view.findViewById(R.id.leapfrog_views_container) - val leapfrog = LeapfrogWidget(leapfrogContainer) - twinsCardWidget = TwinsCardWidget(leapfrog) { 200f } - - binding.btnTwinWelcome.setOnClickListener { - twinsCardWidget.toWelcome() - } - binding.btnTwinToLeapfrog.setOnClickListener { - twinsCardWidget.toLeapfrog() - } - binding.btnTwinActivate.setOnClickListener { - twinsCardWidget.toActivate() - } - - binding.btnLpInit.setOnClickListener { - leapfrog.initViews() - } - binding.btnLpUnfold.setOnClickListener { - leapfrog.unfold() - } - binding.btnLpFold.setOnClickListener { - leapfrog.fold() - } - binding.btnLpLeap.setOnClickListener { - leapfrog.leap() - } - binding.btnLpLeapBack.setOnClickListener { - leapfrog.leapBack() - } - } - - override fun onStart() { - super.onStart() - leapfrogWidgetState?.let { twinsCardWidget.leapfrogWidget.applyState(it) } - } - - override fun onStop() { - super.onStop() - leapfrogWidgetState = twinsCardWidget.leapfrogWidget.getState() - } -} - -private var leapfrogWidgetState: LeapfrogWidgetState? = null \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/recyclerView/SpaceItemDecoration.kt b/app/src/main/java/com/tangem/tap/common/recyclerView/SpaceItemDecoration.kt deleted file mode 100644 index 4ea08ea19e..0000000000 --- a/app/src/main/java/com/tangem/tap/common/recyclerView/SpaceItemDecoration.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tangem.tap.common.recyclerView - -import android.graphics.Rect -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.tangem.sdk.extensions.dpToPx - -class SpaceItemDecoration( - private val horizontalSpaceDp: Float, - private val verticalSpaceDp: Float, -) : RecyclerView.ItemDecoration() { - - private lateinit var space: Space - - override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { - if (state.itemCount == 0) return - if (!::space.isInitialized) { - space = Space( - view.dpToPx(horizontalSpaceDp).toInt(), - view.dpToPx(verticalSpaceDp).toInt(), - ) - } - - outRect.left = space.horizontal - outRect.right = space.horizontal - - when (state.itemCount) { - 1 -> { - outRect.top = space.vertical - outRect.bottom = space.vertical - } - else -> { - val adapterPosition = parent.getChildAdapterPosition(view) - if (adapterPosition == -1) return - - when (adapterPosition) { - 0 -> { - // first - outRect.top = space.vertical - outRect.bottom = space.vertical / 2 - } - state.itemCount - 1 -> { - // last - outRect.top = space.vertical / 2 - outRect.bottom = space.vertical - } - else -> { - // middle - outRect.top = space.vertical / 2 - outRect.bottom = space.vertical / 2 - } - } - } - } - } - - private data class Space( - val horizontal: Int, - val vertical: Int, - ) - - companion object { - fun all(dp: Float): SpaceItemDecoration = SpaceItemDecoration(dp, dp) - fun vertical(dp: Float): SpaceItemDecoration = SpaceItemDecoration(0f, dp) - fun horizontal(dp: Float): SpaceItemDecoration = SpaceItemDecoration(dp, 0f) - } -} \ No newline at end of file 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 d047620b5e..cdc2bed475 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 @@ -10,7 +10,6 @@ import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOt 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.tokens.legacy.redux.TokensReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer import com.tangem.tap.proxy.AppStateHolder @@ -28,7 +27,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) onboardingWalletState = OnboardingWalletReducer.reduce(action, state), onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state), twinCardsState = TwinCardsReducer.reduce(action, state), - sendState = SendScreenReducer.reduce(action, state.sendState), detailsState = DetailsReducer.reduce(action, state), disclaimerState = DisclaimerReducer.reduce(action, state), tokensState = TokensReducer.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 2a83cbf4df..bdcb0bd1c1 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 @@ -25,8 +25,6 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWallet import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState 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.tokens.legacy.redux.TokensState import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.features.welcome.redux.WelcomeMiddleware @@ -44,7 +42,6 @@ data class AppState( val onboardingWalletState: OnboardingWalletState = OnboardingWalletState(), val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(), val twinCardsState: TwinCardsState = TwinCardsState(), - val sendState: SendState = SendState(), val detailsState: DetailsState = DetailsState(), val disclaimerState: DisclaimerState = DisclaimerState(), val tokensState: TokensState = TokensState(), @@ -77,7 +74,6 @@ data class AppState( OnboardingWalletMiddleware.handler, OnboardingOtherCardsMiddleware.handler, TwinCardsMiddleware.handler, - SendMiddleware().sendMiddleware, DetailsMiddleware().detailsMiddleware, DisclaimerMiddleware().disclaimerMiddleware, WalletConnectMiddleware().walletConnectMiddleware, diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 58d13a6d79..614b69566d 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -12,7 +12,6 @@ import com.tangem.tap.common.redux.DebugErrorAction import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import org.rekotlin.Action @@ -71,8 +70,6 @@ sealed class GlobalAction : Action { val walletPublicKey: ByteArray, ) : GlobalAction() - data class HideWarningMessage(val warning: WarningMessage) : GlobalAction() - data class SetConfigManager(val configManager: ConfigManager) : GlobalAction() data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction() data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction() diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index 9c6882deda..43d85d7fc5 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -12,7 +12,6 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.network.exchangeServices.BuyExchangeService import com.tangem.tap.network.exchangeServices.CardExchangeRules import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager @@ -56,18 +55,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) { is GlobalAction.RestoreAppCurrency -> { restoreAppCurrency() } - is GlobalAction.HideWarningMessage -> { - store.state.globalState.warningManager?.let { - if (it.hideWarning(action.warning)) { - // if (WarningMessagesManager.isAlreadySignedHashesWarning()) { - // // TODO: No appropriate warningMessage identification. Make it better later - // store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) - // } - - store.dispatch(SendAction.Warnings.Update) - } - } - } is GlobalAction.SendEmail -> { store.state.globalState.feedbackManager?.sendEmail( feedbackData = action.feedbackData, diff --git a/app/src/main/java/com/tangem/tap/common/snackBar/MaxAmountSnackbar.kt b/app/src/main/java/com/tangem/tap/common/snackBar/MaxAmountSnackbar.kt deleted file mode 100644 index 939fc17c61..0000000000 --- a/app/src/main/java/com/tangem/tap/common/snackBar/MaxAmountSnackbar.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.tangem.tap.common.snackBar - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updateLayoutParams -import com.google.android.material.snackbar.BaseTransientBottomBar -import com.google.android.material.snackbar.ContentViewCallback -import com.google.android.material.snackbar.Snackbar -import com.tangem.sdk.extensions.dpToPx -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -class MaxAmountSnackbar( - parent: ViewGroup, - content: MaxAmountSnackbarView, -) : BaseTransientBottomBar(parent, content, content) { - - companion object { - - fun make(view: View, onClick: () -> Unit): MaxAmountSnackbar { - val parent = view.findSuitableParent() ?: throw IllegalArgumentException( - "No suitable parent found from the given view. Please provide a valid view.", - ) - val inflater = LayoutInflater.from(parent.context) - val customView = inflater.inflate(R.layout.view_snackbar_max_amount, parent, false) as MaxAmountSnackbarView - customView.setOnClickListener { onClick() } - - return MaxAmountSnackbar(parent, customView).apply { - updateBottomMargin() - duration = Snackbar.LENGTH_INDEFINITE - } - } - - private fun MaxAmountSnackbar.updateBottomMargin() { - ViewCompat.setOnApplyWindowInsetsListener(this.view) { _, insets -> - val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom - val bottomInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom - - this.view.updateLayoutParams { - bottomMargin = imeInsets - bottomInsets + context.dpToPx(dp = 8f).toInt() - } - - insets - } - } - - private fun View?.findSuitableParent(): ViewGroup? { - var view = this - var fallback: ViewGroup? = null - do { - if (view is CoordinatorLayout) { - return view - } else if (view is FrameLayout) { - if (view.id == android.R.id.content) { - return view - } else { - fallback = view - } - } - - if (view != null) { - val parent = view.parent - view = if (parent is View) parent else null - } - } while (view != null) - return fallback - } - } -} - -class MaxAmountSnackbarView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, -) : ConstraintLayout(context, attrs, defStyleAttr), ContentViewCallback { - - init { - View.inflate(context, R.layout.view_snackbar_max_amount_content, this) - clipToPadding = false - } - - override fun animateContentIn(delay: Int, duration: Int) { - } - - override fun animateContentOut(delay: Int, duration: Int) { - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/text/DecimalDigitsInputFilter.kt b/app/src/main/java/com/tangem/tap/common/text/DecimalDigitsInputFilter.kt deleted file mode 100644 index 2201e794cc..0000000000 --- a/app/src/main/java/com/tangem/tap/common/text/DecimalDigitsInputFilter.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.tap.common.text - -import android.text.InputFilter -import android.text.Spanned -import java.util.regex.Pattern - -/** -[REDACTED_AUTHOR] - */ -class DecimalDigitsInputFilter( - digitsBeforeDecimal: Int, - digitsAfterDecimal: Int, - private val decimalSeparator: String, -) : InputFilter { - private val pattern: Pattern = Pattern.compile( - "(([1-9]{1}[0-9]{0,${digitsBeforeDecimal - 1}})?||[0]{1})" + - "((\\$decimalSeparator[0-9]{0,$digitsAfterDecimal})?)||(\\$decimalSeparator)?", - ) - - override fun filter( - source: CharSequence, - sourceStart: Int, - sourceEnd: Int, - destination: Spanned, - destinationStart: Int, - destinationEnd: Int, - ): CharSequence? { - val destString = destination.toString() - val prefix = destString.substring(0, destinationStart) - val suffix = destString.substring(destinationEnd, destString.length) - val newDestination = prefix + suffix - - val resultPrefix = newDestination.substring(0, destinationStart) - val resultSuffix = newDestination.substring(destinationStart, newDestination.length) - val result = resultPrefix + source.toString() + resultSuffix - - return if (pattern.matcher(result).matches()) { - null - } else { - val replacedWithAppropriateDecimalSeparator = setDecimalSeparator(result, decimalSeparator) - if (pattern.matcher(replacedWithAppropriateDecimalSeparator).matches()) { - decimalSeparator - } else { - "" - } - } - } - - companion object { - fun setDecimalSeparator(value: String, decimalSeparator: String): String { - if (value.contains(decimalSeparator)) return value - - return if (decimalSeparator == ".") { - value.replace(",", decimalSeparator) - } else { - value.replace(".", decimalSeparator) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/text/Truncate.kt b/app/src/main/java/com/tangem/tap/common/text/Truncate.kt deleted file mode 100644 index 9131ec1023..0000000000 --- a/app/src/main/java/com/tangem/tap/common/text/Truncate.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.tangem.tap.common.text - -import android.widget.TextView -import com.tangem.tap.common.extensions.isEven - -/** -[REDACTED_AUTHOR] - */ -enum class TruncateType { - START, MIDDLE, END -} - -interface Truncate { - fun apply(tv: TextView, text: String, with: String): String - - companion object { - fun create(type: TruncateType): Truncate { - return when (type) { - TruncateType.START -> TruncateStart() - TruncateType.MIDDLE -> TruncateMiddle() - TruncateType.END -> TruncateEnd() - } - } - } -} - -abstract class BaseTruncate : Truncate { - protected var hasBeenTruncated = false - - override fun apply(tv: TextView, text: String, with: String): String { - val roughLength = getRoughFitLength(tv, text) - val fittedText = preciseFitting(tv, roughTruncate(text, roughLength), with) - return if (hasBeenTruncated) attachWith(fittedText, with) else fittedText - } - - private fun getRoughFitLength(tv: TextView, text: String): Int { - val existingSpace = tv.measuredWidth - (tv.paddingStart + tv.paddingEnd) - val textWillTakeSpace = tv.paint.measureText(text) - val overSizeRatio: Float = textWillTakeSpace / existingSpace - - val maxLengthOfText = text.length / overSizeRatio - if (text.length <= maxLengthOfText) return text.length - - return maxLengthOfText.toInt() - } - - private fun preciseFitting(tv: TextView, text: String, with: String): String { - if (!hasBeenTruncated) return text - - val spaceForText = tv.measuredWidth - (tv.paddingStart + tv.paddingEnd) - - var fittedText = text - while (tv.paint.measureText(fittedText + with) > spaceForText) { - fittedText = preciseTruncate(fittedText) - } - return fittedText - } - - protected abstract fun roughTruncate(text: String, residualLength: Int): String - protected abstract fun preciseTruncate(text: String): String - protected abstract fun attachWith(text: String, with: String): String -} - -class TruncateStart : BaseTruncate() { - override fun roughTruncate(text: String, residualLength: Int): String { - if (text.length <= residualLength) return text - - hasBeenTruncated = true - return text.substring(residualLength, text.length) - } - - override fun preciseTruncate(text: String): String = text.substring(1, text.length) - - override fun attachWith(text: String, with: String): String = with + text -} - -class TruncateMiddle : BaseTruncate() { - - override fun roughTruncate(text: String, residualLength: Int): String { - if (text.length <= residualLength || residualLength < 0) return text - - hasBeenTruncated = true - val halfOfResidualLength = residualLength / 2 - val leftSide = text.substring(0, halfOfResidualLength) - val rightSide = text.substring(text.length - halfOfResidualLength, text.length) - - return leftSide + rightSide - } - - override fun preciseTruncate(text: String): String { - val middlePosition = text.length / 2 - return if (text.length.isEven()) { - val leftSide = text.substring(0, middlePosition - 1) - val rightSide = text.substring(middlePosition, text.length) - leftSide + rightSide - } else { - val leftSide = text.substring(0, middlePosition) - val rightSide = text.substring(middlePosition + 1, text.length) - leftSide + rightSide - } - } - - override fun attachWith(text: String, with: String): String { - val cuttingPosition = text.length / 2 - val leftSide = text.substring(0, cuttingPosition) - val rightSide = text.substring(cuttingPosition, text.length) - return leftSide + with + rightSide - } -} - -class TruncateEnd : BaseTruncate() { - override fun roughTruncate(text: String, residualLength: Int): String { - if (text.length <= residualLength) return text - - hasBeenTruncated = true - return text.substring(0, residualLength) - } - - override fun preciseTruncate(text: String): String = text.substring(0, text.length - 1) - - override fun attachWith(text: String, with: String): String = text + with -} - -fun TextView.truncateWith(text: String, type: TruncateType, with: String = "..."): String { - val truncate = Truncate.create(type) - return truncate.apply(this, text, with) -} - -fun TextView.truncateMiddleWith(text: String, with: String = "..."): String = - this.truncateWith(text, TruncateType.MIDDLE, with) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/toggleWidget/IndeterminateProgressButtonWidget.kt b/app/src/main/java/com/tangem/tap/common/toggleWidget/IndeterminateProgressButtonWidget.kt deleted file mode 100644 index 22817c3f93..0000000000 --- a/app/src/main/java/com/tangem/tap/common/toggleWidget/IndeterminateProgressButtonWidget.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tangem.tap.common.toggleWidget - -import android.graphics.drawable.Drawable -import android.view.View -import com.google.android.material.button.MaterialButton -import com.tangem.tap.common.entities.ProgressState -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.extensions.show - -/** -[REDACTED_AUTHOR] - */ -open class IndeterminateProgressButtonWidget( - private val button: MaterialButton, - private val progress: View, - initialState: ProgressState = ProgressState.Done, -) : ViewStateWidget { - - private var text: CharSequence = button.text - private var icon: Drawable? = button.icon - private var iconGravity: Int? = button.iconGravity - - init { - if (initialState != ProgressState.Done) changeState(initialState) - } - - var isEnabled: Boolean - get() = button.isEnabled - set(value) { - button.isEnabled = value - } - - override val mainView: View = button - - override fun changeState(state: WidgetState) { - val progressState = state as? ProgressState ?: return - - when (progressState) { - ProgressState.Done, ProgressState.Error -> switchToNone() - ProgressState.Loading -> switchToProgress() - else -> {} - } - } - - protected open fun switchToNone() { - button.isClickable = true - button.text = text - button.icon = icon - iconGravity?.let { button.iconGravity = it } - - progress.hide() - } - - protected open fun switchToProgress() { - button.isClickable = false - button.text = "" - button.icon = null - - progress.show() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index 70b2540317..1fc37a0267 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -22,38 +22,14 @@ sealed class TapError( object UnknownError : TapError(R.string.send_error_unknown) open class CustomError(val customMessage: String) : TapError(R.string.common_custom_string, listOf(customMessage)) - data class UnsupportedState( - val stateError: String, - ) : TapError(R.string.common_custom_string, listOf("Unsupported state: $stateError")) object NoInternetConnection : TapError(R.string.wallet_notification_no_internet) - object AmountExceedsBalance : TapError(R.string.send_validation_amount_exceeds_balance) - data class AmountLowerExistentialDeposit( - override val args: List, - ) : TapError(R.string.send_error_minimum_balance_format) - - object FeeExceedsBalance : TapError(R.string.send_validation_invalid_fee) - object TotalExceedsBalance : TapError(R.string.send_validation_invalid_total) - object InvalidAmountValue : TapError(R.string.send_validation_invalid_amount) - object InvalidFeeValue : TapError(R.string.send_error_invalid_fee_value) - data class DustAmount(override val args: List) : TapError(R.string.send_error_dust_amount_format) - object DustChange : TapError(R.string.send_error_dust_change) sealed class WalletManager { class NoAccountError(amountToCreateAccount: String) : CustomError(amountToCreateAccount) class InternalError(message: String) : CustomError(message) object BlockchainIsUnreachableTryLater : TapError(R.string.wallet_balance_blockchain_unreachable_try_later) } - - sealed class WalletConnect { - object UnsupportedDapp : TapError(R.string.wallet_connect_error_unsupported_dapp) - object UnsupportedLink : TapError(R.string.wallet_connect_error_failed_to_connect) - } - - data class ValidateTransactionErrors( - override val errorList: List, - override val builder: (List) -> String, - ) : TapError(-1), MultiMessageError } sealed class TapSdkError(override val messageResId: Int?) : TangemError(code = 50100) { 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 fd2b740b37..60ea643487 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 @@ -18,7 +18,6 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.tap.common.extensions.setContext import com.tangem.tap.features.main.model.MainScreenState @@ -42,7 +41,6 @@ internal class MainViewModel @Inject constructor( private val blockchainSDKFactory: BlockchainSDKFactory, private val userWalletsListManager: UserWalletsListManager, private val walletManagersFacade: WalletManagersFacade, - private val sendFeatureToggles: SendFeatureToggles, private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, private val stakingFeatureToggles: StakingFeatureToggles, @@ -68,7 +66,6 @@ internal class MainViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() } updateAppCurrencies() - updateSendFeatureToggle() observeFlips() displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() @@ -130,12 +127,6 @@ internal class MainViewModel @Inject constructor( } } - private fun updateSendFeatureToggle() { - viewModelScope.launch(dispatchers.main) { - sendFeatureToggles.fetchNewSendEnabled() - } - } - private fun observeFlips() { listenToFlipsUseCase().launchIn(viewModelScope) } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt deleted file mode 100644 index 35ee864987..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ /dev/null @@ -1,215 +0,0 @@ -package com.tangem.tap.features.send.redux - -import com.tangem.Message -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.FeePaidCurrency -import com.tangem.blockchain.common.WalletManager -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.common.core.TangemSdkError -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.StateDialog -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered -import com.tangem.tap.common.redux.ToastNotificationAction -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.tap.features.send.redux.states.ButtonState -import com.tangem.tap.features.send.redux.states.FeeType -import com.tangem.tap.features.send.redux.states.MainCurrencyType -import com.tangem.wallet.R -import org.rekotlin.Action -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -interface SendScreenAction : Action -interface SendScreenActionUi : SendScreenAction - -object ReleaseSendState : Action - -data class PrepareSendScreen( - val walletManager: WalletManager, - val feePaidCurrency: FeePaidCurrency, - val currency: CryptoCurrency, - val coinAmount: Amount?, - val coinRate: BigDecimal?, - val tokenAmount: Amount? = null, - val tokenRate: BigDecimal? = null, - val feeCurrencyRate: BigDecimal? = null, - val feeCurrencyDecimals: Int = 0, -) : SendScreenAction - -// Address -sealed class AddressActionUi : SendScreenActionUi { - data class HandleUserInput(val data: String) : AddressActionUi() - data class PasteAddress(val data: String, val sourceType: AddressEntered.SourceType) : AddressActionUi() - data class CheckClipboard(val data: String?) : AddressActionUi() - data class CheckAddress(val sourceType: AddressEntered.SourceType?) : AddressActionUi() - data class SetTruncateHandler(val handler: (String) -> String) : AddressActionUi() - data class TruncateOrRestore(val truncate: Boolean) : AddressActionUi() -} - -sealed class TransactionExtrasAction : SendScreenActionUi { - data class Prepare( - val blockchain: Blockchain, - val walletAddress: String, - val xrpTag: String?, - ) : TransactionExtrasAction() - - object Release : TransactionExtrasAction() - - @Deprecated("Only in legacy send screen") - sealed class XlmMemo : TransactionExtrasAction() { - // data class ChangeSelectedMemo(val memoType: XlmMemoType) : XlmMemo() - data class HandleUserInput(val data: String) : XlmMemo() - } - - @Deprecated("Only in legacy send screen") - sealed class BinanceMemo : TransactionExtrasAction() { - data class HandleUserInput(val data: String) : BinanceMemo() - } - - @Deprecated("Only in legacy send screen") - sealed class XrpDestinationTag : TransactionExtrasAction() { - data class HandleUserInput(val data: String) : XrpDestinationTag() - } - - @Deprecated("Only in legacy send screen") - sealed class TonMemo : TransactionExtrasAction() { - data class HandleUserInput(val data: String) : TonMemo() - } - - @Deprecated("Only in legacy send screen") - sealed class CosmosMemo : TransactionExtrasAction() { - data class HandleUserInput(val data: String) : CosmosMemo() - } - - @Deprecated("Only in legacy send screen") - sealed class HederaMemo : TransactionExtrasAction() { - data class HandleUserInput(val data: String) : HederaMemo() - } - - @Deprecated("Only in legacy send screen") - sealed class AlgorandMemo : TransactionExtrasAction() { - data class HandleUserInput(val data: String) : AlgorandMemo() - } -} - -sealed class AddressVerifyAction : SendScreenAction { - enum class Error { - ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN, - ADDRESS_SAME_AS_WALLET, - } - - data class ChangePasteBtnEnableState(val isEnabled: Boolean) : AddressVerifyAction() - - sealed class AddressVerification : AddressVerifyAction() { - data class SetAddressError(val error: Error?) : AddressVerification() - data class SetWalletAddress(val address: String, val isUserInput: Boolean) : AddressVerification() - } -} - -// Amount to send -sealed class AmountActionUi : SendScreenActionUi { - data class HandleUserInput(val data: String) : AmountActionUi() - object CheckAmountToSend : AmountActionUi() - object SetMaxAmount : AmountActionUi() - data class SetMainCurrency(val mainCurrency: MainCurrencyType) : AmountActionUi() - object ToggleMainCurrency : AmountActionUi() -} - -sealed class AmountAction : SendScreenAction { - data class SetAmount(val amountCrypto: BigDecimal, val isUserInput: Boolean) : AmountAction() - data class SetAmountError(val error: TapError?) : AmountAction() - data class SetDecimalSeparator(val separator: String) : AmountAction() - data class HideBalance(val hide: Boolean) : AmountAction() -} - -// Fee -sealed class FeeActionUi : SendScreenActionUi { - object ToggleControlsVisibility : FeeActionUi() - data class ChangeSelectedFee(val feeType: FeeType) : FeeActionUi() - class ChangeIncludeFee(val isIncluded: Boolean) : FeeActionUi() -} - -sealed class FeeAction : SendScreenAction { - - object RequestFee : FeeAction() - sealed class FeeCalculation : FeeAction() { - data class SetFeeResult(val fee: TransactionFee) : FeeCalculation() - object ClearResult : FeeCalculation() - } - - data class ChangeLayoutVisibility( - val main: Boolean? = null, - val controls: Boolean? = null, - val chipGroup: Boolean? = null, - ) : FeeAction() -} - -sealed class ReceiptAction : SendScreenAction { - object RefreshReceipt : ReceiptAction() -} - -sealed class SendActionUi : SendScreenActionUi { - data class SendAmountToRecipient(val messageForSigner: Message) : SendScreenActionUi - object CheckIfTransactionDataWasProvided : SendScreenActionUi -} - -sealed class SendAction : SendScreenAction { - - data class ChangeSendButtonState(val state: ButtonState) : SendAction() - object SendSuccess : SendAction(), ToastNotificationAction { - override val messageResource: Int = R.string.send_transaction_success - } - - sealed class Dialog : SendAction(), StateDialog { - data class TezosWarningDialog( - val reduceCallback: () -> Unit, - val sendAllCallback: () -> Unit, - val reduceAmount: BigDecimal, - ) : Dialog() - - data class KaspaWarningDialog( - val maxOutputs: Int, - val maxAmount: BigDecimal, - val onOk: () -> Unit, - ) : Dialog() - - data class ChiaWarningDialog( - val blockchainName: String, - val maxOutputs: Int, - val maxAmount: BigDecimal, - val onOk: () -> Unit, - ) : Dialog() - - sealed class SendTransactionFails : Dialog() { - data class CardSdkError(val error: TangemSdkError, val scanResponse: ScanResponse) : Dialog() - data class BlockchainSdkError( - val error: com.tangem.blockchain.common.BlockchainSdkError, - val scanResponse: ScanResponse, - ) : Dialog() - } - - data class RequestFeeError( - val error: com.tangem.blockchain.common.BlockchainSdkError, - val scanResponse: ScanResponse, - val onRetry: () -> Unit, - ) : Dialog() - - object Hide : Dialog() - } - - sealed class Warnings : SendAction() { - object Update : SendAction() - data class Set(val warningList: List) : SendAction() - } - - data class SendSpecificTransaction( - val sendAmount: String, - val destinationAddress: String, - val transactionId: String, - ) : SendAction() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressMiddleware.kt deleted file mode 100644 index 96cf7c9c6f..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressMiddleware.kt +++ /dev/null @@ -1,225 +0,0 @@ -package com.tangem.tap.features.send.redux.middlewares - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManager -import com.tangem.core.analytics.Analytics -import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.send.redux.* -import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetAddressError -import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetWalletAddress -import com.tangem.tap.features.send.redux.AddressVerifyAction.Error -import com.tangem.tap.mainScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.rekotlin.Action -import org.rekotlin.DispatchFunction - -/** -[REDACTED_AUTHOR] - */ -internal class AddressMiddleware { - - private val addressValidator = AddressValidator() - - fun handle(action: AddressActionUi, appState: AppState?, dispatch: (Action) -> Unit) { - when (action) { - is AddressActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch) - is AddressActionUi.PasteAddress -> pasteAddress(action.data, action.sourceType, dispatch) - is AddressActionUi.CheckClipboard -> verifyClipboard(action.data, appState, dispatch) - is AddressActionUi.CheckAddress -> verifyAddress(action.sourceType, appState, dispatch) - else -> return - } - } - - private fun handleUserInput(input: String, appState: AppState?, dispatch: DispatchFunction) { - val sendState = appState?.sendState ?: return - if (input == sendState.addressState.viewFieldValue.value) return - - setAddressAndCheck(data = input, sourceType = null, isUserInput = true, dispatch = dispatch) - } - - private fun pasteAddress(data: String, sourceType: AddressEntered.SourceType, dispatch: (Action) -> Unit) { - setAddressAndCheck(data = data, sourceType = sourceType, isUserInput = false, dispatch = dispatch) - } - - private fun setAddressAndCheck( - data: String, - sourceType: AddressEntered.SourceType?, - isUserInput: Boolean, - dispatch: (Action) -> Unit, - ) { - dispatch(SetWalletAddress(data, isUserInput)) - dispatch(AddressActionUi.CheckAddress(sourceType)) - } - - private fun verifyAddress( - sourceType: AddressEntered.SourceType?, - appState: AppState?, - dispatch: (Action) -> Unit, - ) { - val sendState = appState?.sendState ?: return - val walletManager = sendState.walletManager ?: return - val address = sendState.addressState.normalFieldValue ?: return - val isUserInput = sendState.addressState.viewFieldValue.isFromUserInput - - verifyAddress( - address = address, - walletManager = walletManager, - isUserInput = isUserInput, - dispatch = dispatch, - sourceType = sourceType, - ) - } - - private fun verifyAddress( - address: String, - walletManager: WalletManager, - sourceType: AddressEntered.SourceType?, - isUserInput: Boolean, - dispatch: (Action) -> Unit, - ) { - val wallet = walletManager.wallet - - mainScope.launch { - val failReason = withContext(Dispatchers.IO) { - addressValidator.validateAddress(walletManager, address) - } - - if (failReason == null) { - dispatchSuccessValidationActions( - address = address, - wallet = wallet, - sourceType = sourceType, - isUserInput = isUserInput, - dispatch = dispatch, - ) - } else { - dispatchFailedValidationActions( - failReason = failReason, - sourceType = sourceType, - dispatch = dispatch, - ) - } - } - } - - private fun dispatchSuccessValidationActions( - address: String, - wallet: Wallet, - sourceType: AddressEntered.SourceType?, - isUserInput: Boolean, - dispatch: (Action) -> Unit, - ) { - val addressSchemeSplit = when (wallet.blockchain) { - Blockchain.BitcoinCash, Blockchain.Kaspa -> listOf(address) - else -> address.split(":") - } - - val noSchemeAddress = when (addressSchemeSplit.size) { - 1 -> address // no scheme - 2 -> { // scheme - if (wallet.blockchain.validateShareScheme(addressSchemeSplit[0])) { - addressSchemeSplit[1] - } else { - sourceType?.let { - Analytics.send( - event = AddressEntered( - sourceType = sourceType, - validationResult = AddressEntered.ValidationResult.Fail, - ), - ) - } - dispatch(SetAddressError(Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN)) - return - } - } - else -> { // invalid URI - sourceType?.let { - Analytics.send( - event = AddressEntered( - sourceType = sourceType, - validationResult = AddressEntered.ValidationResult.Fail, - ), - ) - } - dispatch(SetAddressError(Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN)) - return - } - } - - val supposedAddress = noSchemeAddress.removeShareUriQuery() // TODO: parse query? - - noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let { - dispatch(AmountAction.SetAmount(it, false)) - dispatch(AmountActionUi.CheckAmountToSend) - } - dispatch(SetWalletAddress(supposedAddress, isUserInput)) - dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null)) - dispatch(FeeAction.RequestFee) - sourceType?.let { - Analytics.send( - event = AddressEntered( - sourceType = sourceType, - validationResult = AddressEntered.ValidationResult.Success, - ), - ) - } - } - - private fun dispatchFailedValidationActions( - failReason: Error, - sourceType: AddressEntered.SourceType?, - dispatch: (Action) -> Unit, - ) { - dispatch(SetAddressError(failReason)) - dispatch(TransactionExtrasAction.Release) - sourceType?.let { - Analytics.send( - event = AddressEntered( - sourceType = sourceType, - validationResult = AddressEntered.ValidationResult.Fail, - ), - ) - } - } - - private fun String.removeShareUriQuery(): String = this.substringBefore("?") - - private fun String.getQueryParameter(name: String): String? { - return this.substringAfter("?").splitToMap("&", "=")[name] - } - - private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) { - val address = input ?: return - val walletManager = appState?.sendState?.walletManager ?: return - - val internalDispatcher: (Action) -> Unit = { - when (it) { - is SetWalletAddress -> { - dispatch(AddressVerifyAction.ChangePasteBtnEnableState(true)) - } - is SetAddressError -> { - dispatch(AddressVerifyAction.ChangePasteBtnEnableState(false)) - } - } - } - - verifyAddress( - address = address, - walletManager = walletManager, - sourceType = null, - isUserInput = false, - dispatch = internalDispatcher, - ) - } -} - -fun String.splitToMap(firstDelimiter: String, secondDelimiter: String): Map { - return this - .split(firstDelimiter) - .map { it.split(secondDelimiter) } - .associate { it.first() to it.last() } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressValidator.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressValidator.kt deleted file mode 100644 index 195250f789..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressValidator.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.tangem.tap.features.send.redux.middlewares - -import com.tangem.blockchain.blockchains.near.NearWalletManager -import com.tangem.blockchain.blockchains.near.network.NearAccount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManager -import com.tangem.blockchain.extensions.Result -import com.tangem.tap.features.send.redux.AddressVerifyAction - -internal class AddressValidator { - - suspend fun validateAddress(walletManager: WalletManager, address: String): AddressVerifyAction.Error? { - val blockchain = walletManager.wallet.blockchain - val wallet = walletManager.wallet - - return if (blockchain.isNear()) { - validateNearAddress(walletManager, address) - } else { - validateAddress(wallet, address) - } - } - - private suspend fun validateNearAddress( - walletManager: WalletManager, - address: String, - ): AddressVerifyAction.Error? { - // implicit address validation - if (address.length == NEAR_IMPLICIT_ADDRESS_LENGTH && hexRegex.matches(address)) { - return validateAddress(walletManager.wallet, address) - } - - // named address validation - if (address.length in NEAR_MIN_ADDRESS_LENGTH until NEAR_IMPLICIT_ADDRESS_LENGTH && - nearAddressRegex.matches(address) - ) { - val result = (walletManager as? NearWalletManager)?.getAccount(address) - return if (result is Result.Success && result.data is NearAccount.Full) { - null - } else { - AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN - } - } - - return AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN - } - - private fun Blockchain.isNear(): Boolean { - return this == Blockchain.Near || this == Blockchain.NearTestnet - } - - private fun validateAddress(wallet: Wallet, address: String): AddressVerifyAction.Error? { - return if (wallet.blockchain.validateAddress(address)) { - if (wallet.addresses.all { it.value != address }) { - null - } else { - AddressVerifyAction.Error.ADDRESS_SAME_AS_WALLET - } - } else { - AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN - } - } - - companion object { - private const val NEAR_MIN_ADDRESS_LENGTH = 2 - private const val NEAR_IMPLICIT_ADDRESS_LENGTH = 64 - private val hexRegex = Regex("^[0-9a-f]+$") - private val nearAddressRegex = Regex("^(([a-z\\d]+[\\-_])*[a-z\\d]+\\.)*([a-z\\d]+[\\-_])*[a-z\\d]+\$") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt deleted file mode 100644 index 0564218bcd..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt +++ /dev/null @@ -1,131 +0,0 @@ -package com.tangem.tap.features.send.redux.middlewares - -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.TransactionError -import com.tangem.common.extensions.isZero -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.send.redux.AmountAction -import com.tangem.tap.features.send.redux.AmountActionUi -import com.tangem.tap.features.send.redux.FeeAction -import com.tangem.tap.features.send.redux.FeeActionUi -import com.tangem.tap.features.send.redux.ReceiptAction -import com.tangem.tap.features.send.redux.SendAction -import com.tangem.tap.features.send.redux.states.MainCurrencyType -import com.tangem.tap.features.send.redux.states.SendState -import org.rekotlin.Action -import java.math.BigDecimal -import java.util.* - -/** -[REDACTED_AUTHOR] - */ -class AmountMiddleware { - - fun handle(action: AmountActionUi, appState: AppState?, dispatch: (Action) -> Unit) { - when (action) { - is AmountActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch) - is AmountActionUi.CheckAmountToSend -> checkAmountToSend(appState, dispatch) - is AmountActionUi.SetMaxAmount -> setMaxAmount(appState, dispatch) - is AmountActionUi.ToggleMainCurrency -> toggleMainCurrency(appState, dispatch) - is AmountActionUi.SetMainCurrency -> return - } - } - - private fun handleUserInput(data: String, appState: AppState?, dispatch: (Action) -> Unit) { - val sendState = appState?.sendState ?: return - val amountState = sendState.amountState - - var input = amountState.toBigDecimalSeparator(data) - input = if (input == ".") "0.0" else input - val inputValue = when { - input.isEmpty() || input == "0" -> BigDecimal.ZERO - else -> BigDecimal(input) - } - if (inputValue.isZero() && amountState.amountToSendCrypto.isZero()) return - - val inputValueCrypto = if (amountState.mainCurrency.type == MainCurrencyType.CRYPTO) { - inputValue - } else { - sendState.convertFiatToExtractCrypto(inputValue) - } - - dispatch(AmountAction.SetAmount(inputValueCrypto, true)) - dispatch(AmountActionUi.CheckAmountToSend) - dispatch(FeeAction.RequestFee) - } - - private fun checkAmountToSend(appState: AppState?, dispatch: (Action) -> Unit) { - val sendState = appState?.sendState ?: return - val walletManager = sendState.walletManager ?: return - val typedAmount = sendState.amountState.amountToExtract ?: return - - val inputCrypto = sendState.amountState.amountToSendCrypto - if (sendState.amountState.viewAmountValue.value == "0" && inputCrypto.isZero()) { - dispatch(ReceiptAction.RefreshReceipt) - dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState())) - return - } - - val amountToSend = Amount(typedAmount, sendState.getTotalAmountToSend(inputCrypto)) - val transactionErrors = walletManager.validateTransaction(amountToSend, sendState.feeState.currentFee?.amount) - val amountFieldErrors = filterErrorsForAmountField(transactionErrors) - if (amountFieldErrors.isEmpty()) { - dispatch(AmountAction.SetAmountError(null)) - } else { - dispatch(AmountAction.SetAmountError(createValidateTransactionError(amountFieldErrors, walletManager))) - } - dispatch(ReceiptAction.RefreshReceipt) - dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState())) - } - - private fun setMaxAmount(appState: AppState?, dispatch: (Action) -> Unit) { - val sendState = appState?.sendState ?: return - - dispatch(AmountAction.SetAmount(sendState.amountState.balanceCrypto, false)) - if (sendState.amountState.canIncludeFee()) { - dispatch(FeeAction.ChangeLayoutVisibility(main = true, controls = true)) - dispatch(FeeActionUi.ChangeIncludeFee(true)) - } - - dispatch(AmountActionUi.CheckAmountToSend) - if (SendState.isReadyToRequestFee()) dispatch(FeeAction.RequestFee) - } - - private fun toggleMainCurrency(appState: AppState?, dispatch: (Action) -> Unit) { - val amountState = appState?.sendState?.amountState ?: return - if (!appState.sendState.coinIsConvertible()) return - - val type = if (amountState.mainCurrency.type == MainCurrencyType.FIAT) { - MainCurrencyType.CRYPTO - } else { - MainCurrencyType.FIAT - } - - dispatch(AmountActionUi.SetMainCurrency(type)) - } -} - -private fun filterErrorsForAmountField(errors: EnumSet): EnumSet { - val showIntoAmountField = EnumSet.noneOf(TransactionError::class.java) - errors.forEach { - when (it) { - TransactionError.AmountExceedsBalance -> { - showIntoAmountField.remove(TransactionError.TotalExceedsBalance) - showIntoAmountField.add(it) - } - TransactionError.FeeExceedsBalance -> { - showIntoAmountField.remove(TransactionError.TotalExceedsBalance) - showIntoAmountField.add(it) - } - TransactionError.TotalExceedsBalance -> { - val notAcceptable = listOf(TransactionError.AmountExceedsBalance, TransactionError.FeeExceedsBalance) - if (!showIntoAmountField.containsAll(notAcceptable)) showIntoAmountField.add(it) - showIntoAmountField.remove(TransactionError.AmountLowerExistentialDeposit) - } - else -> showIntoAmountField.add(it) - } - } - showIntoAmountField.remove(TransactionError.TezosSendAll) - - return showIntoAmountField -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt deleted file mode 100644 index ea4092c252..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt +++ /dev/null @@ -1,132 +0,0 @@ -package com.tangem.tap.features.send.redux.middlewares - -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.BlockchainSdkError -import com.tangem.blockchain.common.WalletManager -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.blockchain.extensions.Result -import com.tangem.common.extensions.isZero -import com.tangem.domain.demo.DemoTransactionSender -import com.tangem.domain.feedback.models.BlockchainErrorInfo -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.extensions.stripZeroPlainString -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.demo.isDemoCard -import com.tangem.tap.features.send.redux.AmountActionUi -import com.tangem.tap.features.send.redux.FeeAction -import com.tangem.tap.features.send.redux.ReceiptAction -import com.tangem.tap.features.send.redux.SendAction -import com.tangem.tap.features.send.redux.states.SendState -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.rekotlin.DispatchFunction -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -class RequestFeeMiddleware { - - @Suppress("CyclomaticComplexMethod") - fun handle(appState: AppState?, dispatch: DispatchFunction) { - val sendState = appState?.sendState ?: return - val walletManager = sendState.walletManager ?: return - val scanResponse = appState.globalState.scanResponse ?: return - - if (!SendState.isReadyToRequestFee()) { - dispatch(FeeAction.FeeCalculation.ClearResult) - dispatch(ReceiptAction.RefreshReceipt) - dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState())) - return - } - val typedAmount = sendState.amountState.amountToExtract ?: return - - val destinationAddress = sendState.addressState.destinationWalletAddress!! - val destinationAmount = Amount(typedAmount, sendState.amountState.amountToSendCrypto) - val txSender = if (scanResponse.isDemoCard()) { - DemoTransactionSender(walletManager) - } else { - walletManager - } - scope.launch { - val feeResult = txSender.getFee(destinationAmount, destinationAddress) - withContext(Dispatchers.Main) { - when (feeResult) { - is Result.Success -> { - val result = feeResult.data -// val result = FeeMock.getFee(walletManager.wallet.blockchain) - dispatch(FeeAction.FeeCalculation.SetFeeResult(result)) - when (result) { - is TransactionFee.Single -> { - val fee = result.normal.amount.value ?: BigDecimal.ZERO - if (fee.isZero()) { - dispatch(FeeAction.ChangeLayoutVisibility(main = false)) - } else { - dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = false)) - } - } - is TransactionFee.Choosable -> { - dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = true)) - } - } - } - is Result.Failure -> { - dispatch(FeeAction.FeeCalculation.ClearResult) - dispatch(FeeAction.ChangeLayoutVisibility(main = false)) - - val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles) - if (featureToggles.isLocalLogsEnabled) { - saveBlockchainError(feeResult, destinationAddress, destinationAmount, walletManager) - } else { - store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( - walletManager = walletManager, - amountToSend = destinationAmount, - feeAmount = null, - destinationAddress = destinationAddress, - ) - } - - val blockchainSdkError = feeResult.error as? BlockchainSdkError ?: return@withContext - dispatch( - SendAction.Dialog.RequestFeeError( - error = blockchainSdkError, - scanResponse = scanResponse, - onRetry = { dispatch(FeeAction.RequestFee) }, - ), - ) - } - } - dispatch(AmountActionUi.CheckAmountToSend) - } - } - } - - private fun saveBlockchainError( - feeResult: Result.Failure, - destinationAddress: String, - destinationAmount: Amount, - walletManager: WalletManager, - ) { - store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke( - error = BlockchainErrorInfo( - errorMessage = (feeResult.error as? BlockchainSdkError)?.customMessage - ?: "It isn't BlockchainSdkError", - blockchainId = walletManager.wallet.blockchain.id, - derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "", - destinationAddress = destinationAddress, - tokenSymbol = if (destinationAmount.type is AmountType.Token) { - destinationAmount.currencySymbol - } else { - "" - }, - amount = destinationAmount.value?.stripZeroPlainString() ?: "0", - fee = null, - ), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt deleted file mode 100644 index ecb0e071d8..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ /dev/null @@ -1,455 +0,0 @@ -package com.tangem.tap.features.send.redux.middlewares - -import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras -import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras -import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras -import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras -import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider -import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras -import com.tangem.blockchain.blockchains.ton.TonTransactionExtras -import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder.XrpTransactionExtras -import com.tangem.blockchain.common.* -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.extensions.Result -import com.tangem.blockchainsdk.utils.minimalAmount -import com.tangem.common.core.TangemSdkError -import com.tangem.common.routing.AppRouter -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.domain.common.TapWorkarounds.isStart2Coin -import com.tangem.domain.common.extensions.withMainContext -import com.tangem.domain.demo.DemoTransactionSender -import com.tangem.domain.feedback.models.BlockchainErrorInfo -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.tap.common.analytics.events.Token -import com.tangem.tap.common.analytics.events.Token.Send.SelectedCurrency.CurrencyType -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.TangemSigner -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.tap.features.demo.isDemoCard -import com.tangem.tap.features.send.redux.* -import com.tangem.tap.features.send.redux.FeeAction.RequestFee -import com.tangem.tap.features.send.redux.states.* -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE -import com.tangem.wallet.R -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.rekotlin.Action -import org.rekotlin.Middleware -import java.util.EnumSet - -/** -[REDACTED_AUTHOR] - */ -class SendMiddleware { - val sendMiddleware: Middleware = { dispatch, appState -> - { nextDispatch -> - { action -> - when (action) { - is AddressActionUi -> AddressMiddleware().handle(action, appState(), dispatch) - is AmountActionUi -> AmountMiddleware().handle(action, appState(), dispatch) - is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch) - is SendActionUi.SendAmountToRecipient -> - verifyAndSendTransaction(action, appState(), dispatch) - is SendAction.Warnings.Update -> updateWarnings(dispatch) - is SendActionUi.CheckIfTransactionDataWasProvided -> { - val transactionData = appState()?.sendState?.externalTransactionData - if (transactionData != null) { - store.dispatchOnMain( - AddressVerifyAction.AddressVerification.SetWalletAddress( - address = transactionData.destinationAddress, - isUserInput = false, - ), - ) - store.dispatchOnMain(AmountActionUi.SetMainCurrency(MainCurrencyType.CRYPTO)) - store.dispatchOnMain(AmountActionUi.HandleUserInput(transactionData.amount)) - store.dispatchOnMain( - AmountAction.SetAmount( - transactionData.amount.toBigDecimal(), - false, - ), - ) - } - } - } - nextDispatch(action) - } - } - } -} - -private fun verifyAndSendTransaction( - action: SendActionUi.SendAmountToRecipient, - appState: AppState?, - dispatch: (Action) -> Unit, -) { - val sendState = appState?.sendState ?: return - val walletManager = sendState.walletManager ?: return - val card = appState.globalState.scanResponse?.card ?: return - val destinationAddress = sendState.addressState.destinationWalletAddress ?: return - val typedAmount = sendState.amountState.amountToExtract ?: return - val fee = sendState.feeState.currentFee ?: return - - val amountToSend = Amount(typedAmount, sendState.getTotalAmountToSend()) - - val transactionErrors = walletManager.validateTransaction(amountToSend, fee.amount) - when { - transactionErrors.contains(TransactionError.TezosSendAll) -> { - val reduceAmount = walletManager.wallet.blockchain.minimalAmount() - dispatch( - SendAction.Dialog.TezosWarningDialog( - reduceCallback = { - dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false)) - dispatch(AmountActionUi.CheckAmountToSend) - }, - sendAllCallback = { - sendTransaction( - action = action, - walletManager = walletManager, - amountToSend = amountToSend, - fee = fee, - feeType = sendState.feeState.selectedFeeType, - destinationAddress = destinationAddress, - transactionExtras = sendState.transactionExtrasState, - card = card, - externalTransactionData = sendState.externalTransactionData, - mainCurrencyType = sendState.amountState.mainCurrency.type, - dispatch = dispatch, - ) - }, - reduceAmount, - ), - ) - } - else -> { - sendTransaction( - action = action, - walletManager = walletManager, - amountToSend = amountToSend, - fee = fee, - feeType = sendState.feeState.selectedFeeType, - destinationAddress = destinationAddress, - transactionExtras = sendState.transactionExtrasState, - card = card, - externalTransactionData = sendState.externalTransactionData, - mainCurrencyType = sendState.amountState.mainCurrency.type, - dispatch = dispatch, - ) - } - } -} - -@Suppress("LongParameterList", "LongMethod", "ComplexMethod") -private fun sendTransaction( - action: SendActionUi.SendAmountToRecipient, - walletManager: WalletManager, - amountToSend: Amount, - fee: Fee, - feeType: FeeType, - destinationAddress: String, - transactionExtras: TransactionExtrasState, - card: CardDTO, - externalTransactionData: ExternalTransactionData?, - mainCurrencyType: MainCurrencyType, - dispatch: (Action) -> Unit, -) { - dispatch(SendAction.ChangeSendButtonState(ButtonState.PROGRESS)) - var txData = walletManager.createTransaction(amountToSend, fee, destinationAddress) - - transactionExtras.xlmMemo?.memo?.let { txData = txData.copy(extras = StellarTransactionExtras(it)) } - transactionExtras.binanceMemo?.memo?.let { txData = txData.copy(extras = BinanceTransactionExtras(it.toString())) } - transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionExtras(it)) } - transactionExtras.cosmosMemoState?.memo?.let { txData = txData.copy(extras = CosmosTransactionExtras(it)) } - transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) } - transactionExtras.hederaMemoState?.memo?.let { txData = txData.copy(extras = HederaTransactionExtras(it)) } - transactionExtras.algorandMemoState?.memo?.let { txData = txData.copy(extras = AlgorandTransactionExtras(it)) } - - scope.launch { - // TODO: Risky commented this part, unknown logic, need to test if removed - // TODO: [REDACTED_JIRA] - // val updateWalletResult = walletManager.safeUpdate() - // if (updateWalletResult is Result.Failure) { - // withMainContext { - // when (val error = updateWalletResult.error) { - // is TapError -> store.dispatchErrorNotification(error) - // is BlockchainSdkError -> { - // updateFeedbackManagerInfo( - // walletManager = walletManager, - // amountToSend = amountToSend, - // feeAmount = fee.amount, - // destinationAddress = destinationAddress, - // ) - // dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error = error)) - // } - // else -> { - // val tapError = if (error.message == null) { - // TapError.UnknownError - // } else { - // TapError.CustomError(error.message!!) - // } - // store.dispatchErrorNotification(tapError) - // } - // } - // dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED)) - // } - // return@launch - // } - - val tangemSdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk - val linkedTerminalState = tangemSdk.config.linkedTerminal - if (card.isStart2Coin) { - tangemSdk.config.linkedTerminal = false - } - - val signer = TangemSigner( - card = card, - tangemSdk = tangemSdk, - initialMessage = action.messageForSigner, - ) { signResponse -> - store.dispatch( - GlobalAction.UpdateWalletSignedHashes( - walletSignedHashes = signResponse.totalSignedHashes, - walletPublicKey = walletManager.wallet.publicKey.seedKey, - remainingSignatures = signResponse.remainingSignatures, - ), - ) - } - val sendResult = try { - if (card.isDemoCard()) { - DemoTransactionSender(walletManager).send(txData, signer) - } else { - (walletManager as TransactionSender).send(txData, signer) - } - } catch (ex: Exception) { - FirebaseCrashlytics.getInstance().recordException(ex) - delay(DELAY_SDK_DIALOG_CLOSE) - withMainContext { - tangemSdk.config.linkedTerminal = linkedTerminalState - dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED)) - store.dispatchErrorNotification(TapError.CustomError(ex.localizedMessage ?: "Unknown error")) - } - return@launch - } - - withMainContext { - dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED)) - tangemSdk.config.linkedTerminal = linkedTerminalState - - when (sendResult) { - is Result.Success -> { - dispatch(SendAction.SendSuccess) - - if (externalTransactionData != null) { - Analytics.send( - Basic.TransactionSent( - sentFrom = AnalyticsParam.TxSentFrom.Sell, - memoType = getMemoType(transactionExtras), - ), - ) - Analytics.sendSelectedCurrencyEvent(mainCurrencyType) - dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)) - } else { - Analytics.send( - Basic.TransactionSent( - sentFrom = AnalyticsParam.TxSentFrom.Send( - blockchain = walletManager.wallet.blockchain.fullName, - token = amountToSend.currencySymbol, - feeType = feeType.convertToAnalyticsFeeType(), - ), - memoType = getMemoType(transactionExtras), - ), - ) - Analytics.sendSelectedCurrencyEvent(mainCurrencyType) - store.dispatchNavigationAction(AppRouter::pop) - } - } - is Result.Failure -> { - updateFeedbackManagerInfo( - sendResult = sendResult.error, - walletManager = walletManager, - amountToSend = amountToSend, - feeAmount = fee.amount, - destinationAddress = destinationAddress, - ) - val error = sendResult.error as? BlockchainSdkError ?: return@withMainContext - - when (error) { - is BlockchainSdkError.WrappedTangemError -> { - val tangemSdkError = error.tangemError as? TangemSdkError ?: return@withMainContext - if (tangemSdkError is TangemSdkError.UserCancelled) return@withMainContext - - dispatch( - SendAction.Dialog.SendTransactionFails.CardSdkError( - error = tangemSdkError, - scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager) - .selectedUserWalletSync?.scanResponse - ?: error("ScanResponse must be not null"), - ), - ) - } - is BlockchainSdkError.CreateAccountUnderfunded -> { - // from XLM, XRP, Polkadot - dispatch( - SendAction.Dialog.SendTransactionFails.BlockchainSdkError( - error = error, - scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager) - .selectedUserWalletSync?.scanResponse - ?: error("ScanResponse must be not null"), - ), - ) - } - is BlockchainSdkError.Kaspa.UtxoAmountError -> { - dispatch( - SendAction.Dialog.KaspaWarningDialog( - maxOutputs = error.maxOutputs, - maxAmount = error.maxAmount, - onOk = { - dispatch(AmountAction.SetAmount(error.maxAmount, isUserInput = false)) - dispatch(AmountActionUi.CheckAmountToSend) - }, - ), - ) - } - is BlockchainSdkError.Chia.UtxoAmountError -> { - dispatch( - SendAction.Dialog.ChiaWarningDialog( - blockchainName = Blockchain.Chia.fullName, - maxOutputs = error.maxOutputs, - maxAmount = error.maxAmount, - onOk = { - dispatch(AmountAction.SetAmount(error.maxAmount, isUserInput = false)) - dispatch(AmountActionUi.CheckAmountToSend) - }, - ), - ) - } - else -> { - when { - error.customMessage.contains(DemoTransactionSender.ID) -> { - store.dispatchDialogShow( - AppDialog.SimpleOkDialogRes( - headerId = R.string.common_done, - messageId = R.string.alert_demo_feature_disabled, - onOk = { store.dispatchNavigationAction(AppRouter::pop) }, - ), - ) - } - else -> { - dispatch( - SendAction.Dialog.SendTransactionFails.BlockchainSdkError( - error = error, - scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager) - .selectedUserWalletSync?.scanResponse - ?: error("ScanResponse must be not null"), - ), - ) - } - } - } - } - } - } - } - } -} - -private fun getMemoType(transactionExtras: TransactionExtrasState): Basic.TransactionSent.MemoType { - return when { - transactionExtras.isEmpty() -> Basic.TransactionSent.MemoType.Empty - transactionExtras.isNull() -> Basic.TransactionSent.MemoType.Null - else -> Basic.TransactionSent.MemoType.Full - } -} - -private fun Analytics.sendSelectedCurrencyEvent(mainCurrencyType: MainCurrencyType) { - send( - Token.Send.SelectedCurrency( - currency = when (mainCurrencyType) { - MainCurrencyType.FIAT -> CurrencyType.AppCurrency - MainCurrencyType.CRYPTO -> CurrencyType.Token - }, - ), - ) -} - -private fun updateFeedbackManagerInfo( - walletManager: WalletManager, - amountToSend: Amount, - feeAmount: Amount, - destinationAddress: String, - sendResult: BlockchainError, -) { - val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles) - if (featureToggles.isLocalLogsEnabled) { - store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke( - error = BlockchainErrorInfo( - errorMessage = (sendResult as? BlockchainSdkError)?.customMessage ?: "It isn't BlockchainSdkError", - blockchainId = walletManager.wallet.blockchain.id, - derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "", - destinationAddress = destinationAddress, - tokenSymbol = if (amountToSend.type is AmountType.Token) { - amountToSend.currencySymbol - } else { - "" - }, - amount = amountToSend.value?.stripZeroPlainString() ?: "0", - fee = feeAmount.value?.stripZeroPlainString() ?: "0", - ), - ) - } else { - store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( - walletManager = walletManager, - amountToSend = amountToSend, - feeAmount = feeAmount, - destinationAddress = destinationAddress, - ) - } -} - -fun createValidateTransactionError( - errorList: EnumSet, - walletManager: WalletManager, -): TapError.ValidateTransactionErrors { - val tapErrors = errorList.map { - when (it) { - TransactionError.AmountExceedsBalance -> TapError.AmountExceedsBalance - TransactionError.AmountLowerExistentialDeposit -> { - if (walletManager is ExistentialDepositProvider) { - val args = listOf(walletManager.getExistentialDeposit().stripZeroPlainString()) - TapError.AmountLowerExistentialDeposit(args) - } else { - TapError.UnknownError - } - } - TransactionError.FeeExceedsBalance -> TapError.FeeExceedsBalance - TransactionError.TotalExceedsBalance -> TapError.TotalExceedsBalance - TransactionError.InvalidAmountValue -> TapError.InvalidAmountValue - TransactionError.InvalidFeeValue -> TapError.InvalidFeeValue - TransactionError.DustAmount -> { - val args = listOf(walletManager.dustValue?.stripZeroPlainString() ?: "0") - TapError.DustAmount(args) - } - TransactionError.DustChange -> TapError.DustChange - else -> TapError.UnknownError - } - } - return TapError.ValidateTransactionErrors(tapErrors) { it.joinToString("\r\n") } -} - -private fun updateWarnings(dispatch: (Action) -> Unit) { - val warningsManager = store.state.globalState.warningManager ?: return - val blockchain = store.state.sendState.walletManager?.wallet?.blockchain ?: return - - val warnings = warningsManager.getWarnings(WarningMessage.Location.SendScreen, listOf(blockchain)) - dispatch(SendAction.Warnings.Set(warnings)) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressReducer.kt deleted file mode 100644 index a5b44bff0f..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressReducer.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.tap.features.send.redux.reducers - -import com.tangem.tap.features.send.redux.AddressActionUi -import com.tangem.tap.features.send.redux.AddressVerifyAction -import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification -import com.tangem.tap.features.send.redux.SendScreenAction -import com.tangem.tap.features.send.redux.states.AddressState -import com.tangem.tap.features.send.redux.states.InputViewValue -import com.tangem.tap.features.send.redux.states.SendState - -/** -[REDACTED_AUTHOR] - */ -class AddressReducer : SendInternalReducer { - override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) { - is AddressActionUi -> handleUiAction(action, sendState, sendState.addressState) - is AddressVerifyAction -> handleAction(action, sendState, sendState.addressState) - else -> sendState - } - - private fun handleUiAction(action: AddressActionUi, sendState: SendState, state: AddressState): SendState { - val result = when (action) { - is AddressActionUi.HandleUserInput -> state - is AddressActionUi.SetTruncateHandler -> state.copy(truncateHandler = action.handler) - is AddressActionUi.TruncateOrRestore -> { - val value = if (action.truncate) state.truncatedFieldValue ?: "" else state.normalFieldValue ?: "" - state.copy(viewFieldValue = state.viewFieldValue.copy(value = value)) - } - is AddressActionUi.PasteAddress -> return sendState - is AddressActionUi.CheckClipboard -> return sendState - is AddressActionUi.CheckAddress -> return sendState - } - return updateLastState(sendState.copy(addressState = result), result) - } - - private fun handleAction(action: AddressVerifyAction, sendState: SendState, state: AddressState): SendState { - val result = when (action) { - is AddressVerification.SetWalletAddress -> { - state.copy( - viewFieldValue = InputViewValue(action.address, action.isUserInput), - normalFieldValue = action.address, - truncatedFieldValue = state.truncate(action.address), - destinationWalletAddress = action.address, - error = null, - ) - } - is AddressVerifyAction.ChangePasteBtnEnableState -> state.copy(pasteIsEnabled = action.isEnabled) - is AddressVerification.SetAddressError -> state.copy(error = action.error, destinationWalletAddress = null) - } - return updateLastState(sendState.copy(addressState = result), result) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt deleted file mode 100644 index 645fc6ec32..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.tangem.tap.features.send.redux.reducers - -import com.tangem.common.extensions.isZero -import com.tangem.tap.common.extensions.stripZeroPlainString -import com.tangem.tap.features.send.redux.AmountAction -import com.tangem.tap.features.send.redux.AmountActionUi -import com.tangem.tap.features.send.redux.AmountActionUi.SetMainCurrency -import com.tangem.tap.features.send.redux.SendScreenAction -import com.tangem.tap.features.send.redux.states.AmountState -import com.tangem.tap.features.send.redux.states.InputViewValue -import com.tangem.tap.features.send.redux.states.MainCurrencyType -import com.tangem.tap.features.send.redux.states.SendState -import com.tangem.utils.StringsSigns.STARS -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -class AmountReducer : SendInternalReducer { - override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) { - is AmountActionUi -> handleUiAction(action, sendState, sendState.amountState) - is AmountAction -> handleAction(action, sendState, sendState.amountState) - else -> sendState - } - - private fun handleUiAction(action: AmountActionUi, sendState: SendState, state: AmountState): SendState { - val result = when (action) { - is SetMainCurrency -> { - val currencyCanBeSwitched = sendState.mainCurrencyCanBeSwitched() - - when (val currency = if (currencyCanBeSwitched) action.mainCurrency else MainCurrencyType.CRYPTO) { - MainCurrencyType.FIAT -> { - val fiatToSend = if (state.amountToSendCrypto.isZero()) { - BigDecimal.ZERO - } else { - sendState.convertExtractCryptoToFiat(state.amountToSendCrypto) - } - val rescaledBalance = sendState.convertExtractCryptoToFiat(state.balanceCrypto, true) - val viewValue = state.restoreDecimalSeparator(fiatToSend.stripZeroPlainString()) - state.copy( - viewAmountValue = InputViewValue(viewValue), - viewBalanceValue = if (state.hideBalance) STARS else rescaledBalance.stripZeroPlainString(), - mainCurrency = state.createMainCurrency(currency, true), - maxLengthOfAmount = sendState.getDecimals(currency), - cursorAtTheSamePosition = false, - ) - } - MainCurrencyType.CRYPTO -> { - val viewValue = state.restoreDecimalSeparator(state.amountToSendCrypto.stripZeroPlainString()) - state.copy( - viewAmountValue = InputViewValue(viewValue), - viewBalanceValue = state.balanceCrypto.stripZeroPlainString(), - mainCurrency = state.createMainCurrency(currency, currencyCanBeSwitched), - maxLengthOfAmount = sendState.getDecimals(currency), - cursorAtTheSamePosition = false, - ) - } - } - } - else -> return sendState - } - - return updateLastState(sendState.copy(amountState = result), result) - } - - private fun handleAction(action: AmountAction, sendState: SendState, state: AmountState): SendState { - val result = when (action) { - is AmountAction.SetAmount -> { - val amount = if (state.mainCurrency.type == MainCurrencyType.CRYPTO) { - action.amountCrypto - } else { - sendState.convertExtractCryptoToFiat(action.amountCrypto, true) - } - val viewValue = state.restoreDecimalSeparator(amount.stripZeroPlainString()) - state.copy( - viewAmountValue = InputViewValue(viewValue, action.isUserInput), - amountToSendCrypto = action.amountCrypto, - cursorAtTheSamePosition = true, - error = null, - ) - } - is AmountAction.SetAmountError -> state.copy(error = action.error) - is AmountAction.SetDecimalSeparator -> state.copy(decimalSeparator = action.separator) - is AmountAction.HideBalance -> { - val rescaledBalance = if (state.mainCurrency.type == MainCurrencyType.CRYPTO) { - state.balanceCrypto - } else { - sendState.convertExtractCryptoToFiat(state.balanceCrypto, true) - } - - state.copy( - hideBalance = action.hide, - viewBalanceValue = if (action.hide) STARS else rescaledBalance.stripZeroPlainString(), - ) - } - } - return updateLastState(sendState.copy(amountState = result), result) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt deleted file mode 100644 index e4ac2c2376..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.tangem.tap.features.send.redux.reducers - -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.tap.common.entities.ProgressState -import com.tangem.tap.features.send.redux.FeeAction -import com.tangem.tap.features.send.redux.FeeActionUi -import com.tangem.tap.features.send.redux.SendScreenAction -import com.tangem.tap.features.send.redux.states.FeeState -import com.tangem.tap.features.send.redux.states.FeeType -import com.tangem.tap.features.send.redux.states.SendState - -/** -[REDACTED_AUTHOR] - */ -class FeeReducer : SendInternalReducer { - - override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) { - is FeeActionUi -> handleUiAction(action, sendState, sendState.feeState) - is FeeAction -> handleAction(action, sendState, sendState.feeState) - else -> sendState - } - - private fun handleUiAction(action: FeeActionUi, sendState: SendState, state: FeeState): SendState { - val result = when (action) { - is FeeActionUi.ToggleControlsVisibility -> { - state.copy(controlsLayoutIsVisible = !state.controlsLayoutIsVisible) - } - is FeeActionUi.ChangeSelectedFee -> { - val currentFee = state.fees?.let { - createValueOfFeeAmount(action.feeType, it) - } - state.copy( - selectedFeeType = action.feeType, - currentFee = currentFee, - ) - } - is FeeActionUi.ChangeIncludeFee -> state.copy(feeIsIncluded = action.isIncluded) - } - return updateLastState(sendState.copy(feeState = result), result) - } - - private fun handleAction(action: FeeAction, sendState: SendState, state: FeeState): SendState { - val result = when (action) { - is FeeAction.RequestFee -> { - state.copy(progressState = ProgressState.Loading) - } - is FeeAction.ChangeLayoutVisibility -> { - fun getVisibility(current: Boolean, proposed: Boolean?): Boolean = proposed ?: current - state.copy( - mainLayoutIsVisible = getVisibility(state.mainLayoutIsVisible, action.main), - controlsLayoutIsVisible = getVisibility(state.controlsLayoutIsVisible, action.controls), - feeChipGroupIsVisible = getVisibility(state.feeChipGroupIsVisible, action.chipGroup), - ) - } - is FeeAction.FeeCalculation.SetFeeResult -> { - when (val fees = action.fee) { - is TransactionFee.Single -> { - val feeType = FeeType.SINGLE - val currentFee = createValueOfFeeAmount(feeType, fees) - - state.copy( - selectedFeeType = feeType, - fees = fees, - currentFee = currentFee, - feeIsApproximate = isFeeApproximate(sendState), - ) - } - is TransactionFee.Choosable -> { - val feeType = getCurrentFeeType(state) - val currentFee = createValueOfFeeAmount(feeType, fees) - - state.copy( - selectedFeeType = feeType, - fees = fees, - currentFee = currentFee, - feeIsApproximate = isFeeApproximate(sendState), - ) - } - }.copy( - progressState = ProgressState.Done, - ) - } - FeeAction.FeeCalculation.ClearResult -> { - state.copy( - fees = null, - currentFee = null, - progressState = ProgressState.Done, - ) - } - } - - return updateLastState(sendState.copy(feeState = result), result) - } - - private fun createValueOfFeeAmount(feeType: FeeType, transactionFee: TransactionFee): Fee { - return when (transactionFee) { - is TransactionFee.Single -> { - transactionFee.normal - } - is TransactionFee.Choosable -> { - when (feeType) { - FeeType.SINGLE -> transactionFee.normal - FeeType.LOW -> transactionFee.minimum - FeeType.NORMAL -> transactionFee.normal - FeeType.PRIORITY -> transactionFee.priority - } - } - } - } - - private fun isFeeApproximate(sendState: SendState): Boolean { - val blockchain = sendState.walletManager?.wallet?.blockchain ?: return false - - val amountType = sendState.amountState.typeOfAmount - return blockchain.isFeeApproximate(amountType) - } - - private fun getCurrentFeeType(state: FeeState): FeeType { - return if (state.selectedFeeType == FeeType.SINGLE) FeeType.NORMAL else state.selectedFeeType - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt deleted file mode 100644 index 8d1d46d4cd..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt +++ /dev/null @@ -1,408 +0,0 @@ -package com.tangem.tap.features.send.redux.reducers - -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.FeePaidCurrency -import com.tangem.blockchain.common.Wallet -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.tap.common.extensions.scaleToFiat -import com.tangem.tap.common.extensions.stripZeroPlainString -import com.tangem.tap.features.send.redux.ReceiptAction.RefreshReceipt -import com.tangem.tap.features.send.redux.SendScreenAction -import com.tangem.tap.features.send.redux.states.* -import com.tangem.tap.store -import com.tangem.utils.StringsSigns.LOWER_SIGN -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -@Suppress("LargeClass") -class ReceiptReducer : SendInternalReducer { - - private lateinit var sendState: SendState - private lateinit var amountState: AmountState - private lateinit var feeState: FeeState - - override fun handle(action: SendScreenAction, sendState: SendState): SendState { - this.sendState = sendState - this.amountState = sendState.amountState - this.feeState = sendState.feeState - - return when (action) { - is RefreshReceipt -> handleRefresh(sendState.receiptState) - else -> sendState - } - } - - private fun handleRefresh(state: ReceiptState): SendState { - val wallet = sendState.walletManager?.wallet ?: return sendState - val feePaidCurrency = wallet.blockchain.feePaidCurrency() - - val layoutType = determineLayoutType(amountState.mainCurrency.type, amountState.typeOfAmount, feePaidCurrency) - val symbols = determineSymbols(wallet, amountState.typeOfAmount, feePaidCurrency) - val showBlank = !SendState.isReadyToSend() - val result = state.copy( - visibleTypeOfReceipt = layoutType, - mainCurrency = amountState.mainCurrency, - fiat = createFiatType(symbols, showBlank), - crypto = createCryptoType(symbols, showBlank), - tokenFiat = createTokenFiatType(symbols, showBlank), - tokenCrypto = createTokenCryptoType(symbols, showBlank), - customTokenFiat = createCustomTokenFiat(symbols, showBlank), - customTokenCrypto = createCustomTokenCrypto(symbols, showBlank), - sameCurrencyFiat = createSameCurrencyFiat(symbols, showBlank), - sameCurrencyCrypto = createSameCurrencyCrypto(symbols, showBlank), - ) - return updateLastState(sendState.copy(receiptState = result), result) - } - - private fun createFiatType(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptFiat { - val feeCrypto = feeState.getCurrentFeeValue() - val feeFiat = convertToFiatPrecision(feeCrypto) - - if (showBlank) { - return ReceiptFiat("0", feeFiat, "0", "0", symbols) - } - - return if (feeState.feeIsIncluded) { - val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto.minus(feeCrypto)) - val totalFiat = convertToFiatPrecision(amountState.amountToSendCrypto) - ReceiptFiat( - amountFiat = amountFiat, - feeFiat = feeFiat, - totalFiat = totalFiat, - willSentCrypto = amountState.amountToSendCrypto.stripZeroPlainString(), - symbols = symbols, - ) - } else { - val totalAmountCrypto = amountState.amountToSendCrypto.plus(feeCrypto) - val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto) - val totalFiat = convertToFiatPrecision(totalAmountCrypto) - ReceiptFiat( - amountFiat = amountFiat, - feeFiat = feeFiat, - totalFiat = totalFiat, - willSentCrypto = totalAmountCrypto.stripZeroPlainString(), - symbols = symbols, - ) - } - } - - private fun createCryptoType(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptCrypto { - val feeCrypto = feeState.getCurrentFeeValue() - val feeFiat = convertToFiatPrecision(feeCrypto) - if (showBlank) { - return ReceiptCrypto("0", feeCrypto.stripZeroPlainString(), "0", "0", "0", symbols) - } - - if (feeState.feeIsIncluded) { - return ReceiptCrypto( - amountCrypto = amountState.amountToSendCrypto.minus(feeCrypto).stripZeroPlainString(), - feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(), - totalCrypto = amountState.amountToSendCrypto.stripZeroPlainString(), - feeFiat = feeFiat, - willSentFiat = convertToFiatPrecision(amountState.amountToSendCrypto), - symbols = symbols, - ) - } else { - val totalCrypto = amountState.amountToSendCrypto.plus(feeCrypto) - return ReceiptCrypto( - amountCrypto = amountState.amountToSendCrypto.stripZeroPlainString(), - feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(), - totalCrypto = totalCrypto.stripZeroPlainString(), - feeFiat = feeFiat, - willSentFiat = convertToFiatPrecision(totalCrypto), - symbols = symbols, - ) - } - } - - private fun createTokenFiatType(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenFiat { - val feeCoin = feeState.getCurrentFeeValue() - if (showBlank) { - val feeFiat = convertToFiatPrecision(feeCoin) - return ReceiptTokenFiat("0", feeFiat, "0", "0", "0", symbols) - } - - val tokensToSend = amountState.amountToSendCrypto - - return if (sendState.coinIsConvertible() && sendState.tokenIsConvertible()) { - val feeFiat = sendState.coinConverter!!.toFiatUnscaled(feeCoin) - val amountFiat = sendState.tokenConverter!!.toFiatUnscaled(tokensToSend) - val totalFiat = amountFiat.plus(feeFiat) - ReceiptTokenFiat( - amountFiat = amountFiat.scaleToFiat(true).stripZeroPlainString(), - feeFiat = feeFiat.scaleToFiat(true).stripZeroPlainString().addPrecisionSign(), - totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(), - willSentToken = tokensToSend.stripZeroPlainString(), - willSentFeeCoin = feeCoin.stripZeroPlainString(), - symbols = symbols, - ) - } else { - ReceiptTokenFiat( - amountFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN, - feeFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN, - totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN, - willSentToken = tokensToSend.stripZeroPlainString(), - willSentFeeCoin = feeCoin.stripZeroPlainString(), - symbols = symbols, - ) - } - } - - private fun createTokenCryptoType(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenCrypto { - val feeCoin = feeState.getCurrentFeeValue() - if (showBlank) { - return ReceiptTokenCrypto("0", feeCoin.stripZeroPlainString(), "0", symbols) - } - val tokensToSend = amountState.amountToSendCrypto - - return if (sendState.coinIsConvertible() && sendState.tokenIsConvertible()) { - val tokenFiat = sendState.tokenConverter!!.toFiatUnscaled(tokensToSend) - val feeFiat = sendState.coinConverter!!.toFiatUnscaled(feeCoin) - val totalFiat = tokenFiat.plus(feeFiat) - - ReceiptTokenCrypto( - amountToken = tokensToSend.stripZeroPlainString(), - feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(), - totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(), - symbols = symbols, - ) - } else { - ReceiptTokenCrypto( - amountToken = tokensToSend.stripZeroPlainString(), - feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(), - totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN, - symbols = symbols, - ) - } - } - - private fun createCustomTokenCrypto(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenCrypto { - val feeValue = feeState.getCurrentFeeValue() - if (showBlank) { - return ReceiptTokenCrypto("0", feeValue.stripZeroPlainString(), "0", symbols) - } - val tokensToSend = amountState.amountToSendCrypto - val amountType = amountState.typeOfAmount - val currencyConverter = when { - amountType is AmountType.Token && sendState.feeIsConvertible() -> sendState.tokenConverter - amountType is AmountType.Coin && sendState.feeIsConvertible() -> sendState.coinConverter - else -> null - } - - return if (currencyConverter != null) { - val currencyFiat = currencyConverter.toFiatUnscaled(tokensToSend) - val feeFiat = sendState.customFeeConverter!!.toFiatUnscaled(feeValue) - val totalFiat = currencyFiat.plus(feeFiat) - - ReceiptTokenCrypto( - amountToken = tokensToSend.stripZeroPlainString(), - feeCoin = feeValue.stripZeroPlainString().addPrecisionSign(), - totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(), - symbols = symbols, - ) - } else { - ReceiptTokenCrypto( - amountToken = tokensToSend.stripZeroPlainString(), - feeCoin = feeValue.stripZeroPlainString().addPrecisionSign(), - totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN, - symbols = symbols, - ) - } - } - - private fun createCustomTokenFiat(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenFiat { - val feeCoin = feeState.getCurrentFeeValue() - if (showBlank) { - val feeFiat = convertToFiatPrecision(feeCoin) - return ReceiptTokenFiat("0", feeFiat, "0", "0", "0", symbols) - } - - val tokensToSend = amountState.amountToSendCrypto - - val amountType = amountState.typeOfAmount - val currencyConverter = when { - amountType is AmountType.Token && sendState.feeIsConvertible() -> sendState.tokenConverter - amountType is AmountType.Coin && sendState.feeIsConvertible() -> sendState.coinConverter - else -> null - } - - return if (currencyConverter != null) { - val feeFiat = sendState.customFeeConverter!!.toFiatUnscaled(feeCoin) - val amountFiat = currencyConverter.toFiatUnscaled(tokensToSend) - val totalFiat = amountFiat.plus(feeFiat) - ReceiptTokenFiat( - amountFiat = amountFiat.scaleToFiat(true).stripZeroPlainString(), - feeFiat = feeFiat.scaleToFiat(true).stripZeroPlainString().addPrecisionSign(), - totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(), - willSentToken = tokensToSend.scaleToFiat(true).stripZeroPlainString(), - willSentFeeCoin = feeCoin.stripZeroPlainString(), - symbols = symbols, - ) - } else { - ReceiptTokenFiat( - amountFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN, - feeFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN, - totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN, - willSentToken = tokensToSend.stripZeroPlainString(), - willSentFeeCoin = feeCoin.stripZeroPlainString(), - symbols = symbols, - ) - } - } - - private fun createSameCurrencyCrypto(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptCrypto { - val feeCrypto = feeState.getCurrentFeeValue() - val feeFiat = convertToFiatPrecision(feeCrypto) - if (showBlank) { - return ReceiptCrypto("0", feeCrypto.stripZeroPlainString(), "0", "0", "0", symbols) - } - val isToken = amountState.typeOfAmount is AmountType.Token - - if (feeState.feeIsIncluded) { - return ReceiptCrypto( - amountCrypto = amountState.amountToSendCrypto.minus(feeCrypto).stripZeroPlainString(), - feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(), - totalCrypto = amountState.amountToSendCrypto.stripZeroPlainString(), - feeFiat = feeFiat, - willSentFiat = convertToFiatPrecision(value = amountState.amountToSendCrypto, isToken = isToken), - symbols = symbols, - ) - } else { - val totalCrypto = amountState.amountToSendCrypto.plus(feeCrypto) - return ReceiptCrypto( - amountCrypto = amountState.amountToSendCrypto.stripZeroPlainString(), - feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(), - totalCrypto = totalCrypto.stripZeroPlainString(), - feeFiat = feeFiat, - willSentFiat = convertToFiatPrecision(value = totalCrypto, isToken = isToken), - symbols = symbols, - ) - } - } - - private fun createSameCurrencyFiat(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptFiat { - val feeCrypto = feeState.getCurrentFeeValue() - val isToken = amountState.typeOfAmount is AmountType.Token - val feeFiat = convertToFiatPrecision(feeCrypto, isToken = isToken) - - if (showBlank) { - return ReceiptFiat("0", feeFiat, "0", "0", symbols) - } - - return if (feeState.feeIsIncluded) { - val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto.minus(feeCrypto), isToken = isToken) - val totalFiat = convertToFiatPrecision(amountState.amountToSendCrypto, isToken = isToken) - ReceiptFiat( - amountFiat = amountFiat, - feeFiat = feeFiat, - totalFiat = totalFiat, - willSentCrypto = amountState.amountToSendCrypto.scaleToFiat(true).stripZeroPlainString(), - symbols = symbols, - ) - } else { - val totalAmountCrypto = amountState.amountToSendCrypto.plus(feeCrypto) - val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto, isToken = isToken) - val totalFiat = convertToFiatPrecision(totalAmountCrypto, isToken = isToken) - ReceiptFiat( - amountFiat = amountFiat, - feeFiat = feeFiat, - totalFiat = totalFiat, - willSentCrypto = totalAmountCrypto.scaleToFiat().stripZeroPlainString(), - symbols = symbols, - ) - } - } - - private fun determineSymbols( - wallet: Wallet, - amountType: AmountType, - feePaidCurrency: FeePaidCurrency, - ): ReceiptSymbols { - return ReceiptSymbols( - fiat = store.state.globalState.appCurrency.code, - crypto = wallet.blockchain.currency, - token = when (amountType) { - is AmountType.Token -> amountType.token.symbol - else -> null - }, - fee = when (feePaidCurrency) { - FeePaidCurrency.Coin -> wallet.blockchain.currency - FeePaidCurrency.SameCurrency -> store.state.sendState.currency?.symbol - is FeePaidCurrency.Token -> feePaidCurrency.token.symbol - is FeePaidCurrency.FeeResource -> feePaidCurrency.currency - }, - ) - } - - private fun determineLayoutType( - mainCurrencyType: MainCurrencyType, - amountType: AmountType, - feePaidCurrency: FeePaidCurrency, - ): ReceiptLayoutType { - return when (mainCurrencyType) { - MainCurrencyType.FIAT -> determineFiatLayoutType(feePaidCurrency, amountType) - MainCurrencyType.CRYPTO -> determineCryptoLayoutType(feePaidCurrency, amountType) - } - } - - private fun determineFiatLayoutType(feePaidCurrency: FeePaidCurrency, amountType: AmountType): ReceiptLayoutType { - return when (feePaidCurrency) { - is FeePaidCurrency.Token -> { - val amountToken = (amountType as? AmountType.Token)?.token - val sameToken = - feePaidCurrency.token.contractAddress.equals(amountToken?.contractAddress, ignoreCase = true) - if (sameToken) ReceiptLayoutType.SAME_CURRENCY_FIAT else ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN_FIAT - } - FeePaidCurrency.Coin -> when (amountType) { - AmountType.Coin -> ReceiptLayoutType.FIAT - is AmountType.Token -> ReceiptLayoutType.TOKEN_FIAT - is AmountType.FeeResource, - AmountType.Reserve, - -> ReceiptLayoutType.UNKNOWN - } - FeePaidCurrency.SameCurrency -> ReceiptLayoutType.SAME_CURRENCY_FIAT - is FeePaidCurrency.FeeResource -> ReceiptLayoutType.SAME_CURRENCY - } - } - - private fun determineCryptoLayoutType(feePaidCurrency: FeePaidCurrency, amountType: AmountType): ReceiptLayoutType { - return when (feePaidCurrency) { - is FeePaidCurrency.Token -> { - val amountToken = (amountType as? AmountType.Token)?.token - val sameToken = - feePaidCurrency.token.contractAddress.equals(amountToken?.contractAddress, ignoreCase = true) - if (sameToken) ReceiptLayoutType.SAME_CURRENCY else ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN - } - FeePaidCurrency.SameCurrency -> ReceiptLayoutType.SAME_CURRENCY - FeePaidCurrency.Coin -> when (amountType) { - AmountType.Coin -> ReceiptLayoutType.CRYPTO - is AmountType.Token -> ReceiptLayoutType.TOKEN_CRYPTO - is AmountType.FeeResource, - AmountType.Reserve, - -> ReceiptLayoutType.UNKNOWN - } - is FeePaidCurrency.FeeResource -> ReceiptLayoutType.UNKNOWN - } - } - - private fun convertToFiatPrecision(value: BigDecimal, isToken: Boolean = false): String { - return when { - !isToken && sendState.coinIsConvertible() -> { - sendState.coinConverter!!.toFiatWithPrecision(value).stripZeroPlainString() - } - isToken && sendState.tokenIsConvertible() -> { - sendState.tokenConverter!!.toFiatWithPrecision(value).stripZeroPlainString() - } - else -> { - BigDecimalFormatter.EMPTY_BALANCE_SIGN - } - } - } - - private fun String.addPrecisionSign(): String { - val result = if (feeState.feeIsApproximate) "$LOWER_SIGN $this" else this - return result.trim() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt deleted file mode 100644 index 9ac9254fec..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.tangem.tap.features.send.redux.reducers - -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.FeePaidCurrency -import com.tangem.tap.common.CurrencyConverter -import com.tangem.tap.common.entities.IndeterminateProgressButton -import com.tangem.tap.features.send.redux.* -import com.tangem.tap.features.send.redux.states.ExternalTransactionData -import com.tangem.tap.features.send.redux.states.IdStateHolder -import com.tangem.tap.features.send.redux.states.SendState -import org.rekotlin.Action -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -interface SendInternalReducer { - fun handle(action: SendScreenAction, sendState: SendState): SendState -} - -object SendScreenReducer { - fun reduce(incomingAction: Action, sendState: SendState): SendState { - if (incomingAction is ReleaseSendState) return SendState() - val action = incomingAction as? SendScreenAction ?: return sendState - - val reducer: SendInternalReducer = when (action) { - is PrepareSendScreen -> PrepareSendScreenStatesReducer() - is AddressActionUi, is AddressVerifyAction -> AddressReducer() - is TransactionExtrasAction -> TransactionExtrasReducer() - is AmountActionUi, is AmountAction -> AmountReducer() - is FeeActionUi, is FeeAction -> FeeReducer() - is ReceiptAction -> ReceiptReducer() - is SendAction -> SendReducer() - else -> EmptyReducer() - } - - return reducer.handle(action, sendState) - } -} - -private class SendReducer : SendInternalReducer { - override fun handle(action: SendScreenAction, sendState: SendState): SendState { - val result = when (action) { - is SendAction.ChangeSendButtonState -> { - sendState.copy(sendButtonState = IndeterminateProgressButton(action.state)) - } - is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action) - is SendAction.Dialog.KaspaWarningDialog -> sendState.copy(dialog = action) - is SendAction.Dialog.ChiaWarningDialog -> sendState.copy(dialog = action) - is SendAction.Dialog.SendTransactionFails.CardSdkError -> sendState.copy(dialog = action) - is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> sendState.copy(dialog = action) - is SendAction.Dialog.RequestFeeError -> sendState.copy(dialog = action) - is SendAction.Dialog.Hide -> sendState.copy(dialog = null) - is SendAction.Warnings.Set -> sendState.copy(sendWarningsList = action.warningList) - is SendAction.SendSpecificTransaction -> handleSendSpecificTransactionAction(action, sendState) - is SendAction.SendSuccess -> sendState.copy(isSuccessSend = true) - else -> return sendState - } - - return updateLastState(result, result) - } - - private fun handleSendSpecificTransactionAction( - action: SendAction.SendSpecificTransaction, - state: SendState, - ): SendState { - return state.copy( - externalTransactionData = - ExternalTransactionData(action.sendAmount, action.destinationAddress, action.transactionId), - feeState = state.feeState.copy( - includeFeeSwitcherIsEnabled = false, - ), - amountState = state.amountState.copy( - inputIsEnabled = false, - ), - addressState = state.addressState.copy( - inputIsEnabled = false, - ), - ) - } -} - -private class EmptyReducer : SendInternalReducer { - override fun handle(action: SendScreenAction, sendState: SendState): SendState = sendState -} - -private class PrepareSendScreenStatesReducer : SendInternalReducer { - override fun handle(action: SendScreenAction, sendState: SendState): SendState { - val prepareAction = action as PrepareSendScreen - val walletManager = action.walletManager - val amountToExtract = prepareAction.tokenAmount ?: prepareAction.coinAmount!! - val decimals = amountToExtract.decimals - val canIncludeFee = canIncludeFee( - typeOfAmount = amountToExtract.type, - feePaidCurrency = action.feePaidCurrency, - ) - - return sendState.copy( - walletManager = walletManager, - coinConverter = action.coinRate?.let { CurrencyConverter(it, decimals) }, - tokenConverter = action.tokenRate?.let { CurrencyConverter(it, decimals) }, - customFeeConverter = action.feeCurrencyRate?.let { CurrencyConverter(it, action.feeCurrencyDecimals) }, - amountState = sendState.amountState.copy( - amountToExtract = amountToExtract, - typeOfAmount = amountToExtract.type, - balanceCrypto = amountToExtract.value ?: BigDecimal.ZERO, - ), - feeState = sendState.feeState.copy(includeFeeSwitcherIsEnabled = canIncludeFee), - canIncludeFee = canIncludeFee, - currency = action.currency, - ) - } - - private fun canIncludeFee(typeOfAmount: AmountType, feePaidCurrency: FeePaidCurrency): Boolean { - return when (feePaidCurrency) { - FeePaidCurrency.Coin -> typeOfAmount == AmountType.Coin - FeePaidCurrency.SameCurrency -> true - is FeePaidCurrency.Token -> { - val sendToken = (typeOfAmount as? AmountType.Token)?.token ?: return false - - sendToken.contractAddress.equals(feePaidCurrency.token.contractAddress, ignoreCase = true) && - sendToken.name.equals(feePaidCurrency.token.name, ignoreCase = true) && - sendToken.symbol.equals(feePaidCurrency.token.symbol, ignoreCase = true) - } - is FeePaidCurrency.FeeResource -> false - } - } -} - -internal fun updateLastState(sendState: SendState, lastChangedState: IdStateHolder): SendState { - sendState.lastChangedStates.add(lastChangedState.stateId) - return sendState -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/TransactionExtrasReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/TransactionExtrasReducer.kt deleted file mode 100644 index c781547049..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/TransactionExtrasReducer.kt +++ /dev/null @@ -1,201 +0,0 @@ -package com.tangem.tap.features.send.redux.reducers - -import com.tangem.blockchain.blockchains.stellar.StellarMemo -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.features.send.redux.SendScreenAction -import com.tangem.tap.features.send.redux.TransactionExtrasAction.* -import com.tangem.tap.features.send.redux.states.* - -/** -[REDACTED_AUTHOR] - */ -class TransactionExtrasReducer : SendInternalReducer { - override fun handle(action: SendScreenAction, sendState: SendState): SendState { - return when (action) { - is Prepare -> handleInitialization(action, sendState) - Release -> handleRelease(sendState) - is XlmMemo -> handleXlmMemo(action, sendState, sendState.transactionExtrasState) - is BinanceMemo -> handleBinanceMemo(action, sendState, sendState.transactionExtrasState) - is XrpDestinationTag -> handleXrpTag(action, sendState, sendState.transactionExtrasState) - is TonMemo -> handleTonMemo(action, sendState, sendState.transactionExtrasState) - is CosmosMemo -> handleCosmosMemo(action, sendState, sendState.transactionExtrasState) - is HederaMemo -> handleHederaMemo(action, sendState, sendState.transactionExtrasState) - is AlgorandMemo -> handleAlgorandMemo(action, sendState, sendState.transactionExtrasState) - else -> sendState - } - } - - private fun handleInitialization(action: Prepare, sendState: SendState): SendState { - val emptyResult = TransactionExtrasState() - val result = when (action.blockchain) { - Blockchain.XRP -> { - val address = action.walletAddress.substringAfter(":") - // 'r' - without tag, 'x' - with tag - if (address.startsWith("r", true)) { - val tag = action.xrpTag?.toLongOrNull() - if (tag == null) { - TransactionExtrasState(xrpDestinationTag = XrpDestinationTagState()) - } else { - TransactionExtrasState( - xrpDestinationTag = XrpDestinationTagState( - viewFieldValue = InputViewValue("$tag", false), - tag = tag, - ), - ) - } - } else { - emptyResult - } - } - Blockchain.Stellar -> TransactionExtrasState(xlmMemo = XlmMemoState()) - Blockchain.Binance -> TransactionExtrasState(binanceMemo = BinanceMemoState()) - Blockchain.TON, Blockchain.TONTestnet -> TransactionExtrasState(tonMemoState = TonMemoState()) - Blockchain.Cosmos, - Blockchain.TerraV1, - Blockchain.TerraV2, - -> TransactionExtrasState(cosmosMemoState = CosmosMemoState()) - Blockchain.Hedera, Blockchain.HederaTestnet -> TransactionExtrasState(hederaMemoState = HederaMemoState()) - Blockchain.Algorand, Blockchain.AlgorandTestnet -> TransactionExtrasState( - algorandMemoState = AlgorandMemoState(), - ) - else -> emptyResult - } - return updateLastState(sendState.copy(transactionExtrasState = result), result) - } - - private fun handleRelease(sendState: SendState): SendState { - val result = TransactionExtrasState() - return updateLastState(sendState.copy(transactionExtrasState = result), result) - } - - private fun handleXlmMemo(action: XlmMemo, sendState: SendState, infoState: TransactionExtrasState): SendState { - fun clearMemo(memo: XlmMemoState): XlmMemoState = memo.copy(text = null, id = null, error = null) - - val result = when (action) { - is XlmMemo.HandleUserInput -> { - val inputViewValue = InputViewValue(action.data, true) - var memo = infoState.xlmMemo?.copy(viewFieldValue = inputViewValue) - ?: XlmMemoState(inputViewValue) - memo = clearMemo(memo) - memo = when (memo.selectedMemoType) { - XlmMemoType.TEXT -> { - if (XlmMemoState.isAssignableValue(action.data)) { - memo.copy(text = StellarMemo.Text(action.data)) - } else { - memo.copy(error = TransactionExtraError.INVALID_XLM_MEMO) - } - } - XlmMemoType.ID -> { - if (XlmMemoState.isAssignableValue(action.data)) { - memo.copy(id = StellarMemo.Id(action.data.toBigInteger())) - } else { - memo.copy(error = TransactionExtraError.INVALID_XLM_MEMO) - } - } - } - infoState.copy(xlmMemo = memo) - } - } - return updateLastState(sendState.copy(transactionExtrasState = result), result) - } - - private fun handleBinanceMemo( - action: BinanceMemo, - sendState: SendState, - infoState: TransactionExtrasState, - ): SendState { - val result = when (action) { - is BinanceMemo.HandleUserInput -> { - val tag = action.data.toBigIntegerOrNull() - if (tag != null) { - val input = InputViewValue(action.data, true) - val tagState = BinanceMemoState(input, tag) - infoState.copy(binanceMemo = tagState) - } else { - infoState - } - } - } - return updateLastState(sendState.copy(transactionExtrasState = result), result) - } - - private fun handleXrpTag( - action: XrpDestinationTag, - sendState: SendState, - infoState: TransactionExtrasState, - ): SendState { - val result = when (action) { - is XrpDestinationTag.HandleUserInput -> { - val tag = action.data.toLongOrNull() - if (tag != null) { - val input = InputViewValue(action.data, true) - val tagState = if (tag <= XrpDestinationTagState.MAX_NUMBER) { - XrpDestinationTagState(input, tag) - } else { - XrpDestinationTagState(input, error = TransactionExtraError.INVALID_DESTINATION_TAG) - } - infoState.copy(xrpDestinationTag = tagState) - } else { - infoState - } - } - } - return updateLastState(sendState.copy(transactionExtrasState = result), result) - } - - private fun handleTonMemo(action: TonMemo, sendState: SendState, infoState: TransactionExtrasState): SendState { - val result = when (action) { - is TonMemo.HandleUserInput -> { - val memo = action.data - val input = InputViewValue(memo, true) - infoState.copy(tonMemoState = TonMemoState(input, memo)) - } - } - return updateLastState(sendState.copy(transactionExtrasState = result), result) - } - - private fun handleCosmosMemo( - action: CosmosMemo, - sendState: SendState, - infoState: TransactionExtrasState, - ): SendState { - val result = when (action) { - is CosmosMemo.HandleUserInput -> { - val memo = action.data - val input = InputViewValue(memo, true) - infoState.copy(cosmosMemoState = CosmosMemoState(input, memo)) - } - } - return updateLastState(sendState.copy(transactionExtrasState = result), result) - } - - private fun handleHederaMemo( - action: HederaMemo, - sendState: SendState, - infoState: TransactionExtrasState, - ): SendState { - val result = when (action) { - is HederaMemo.HandleUserInput -> { - val memo = action.data - val input = InputViewValue(memo, true) - infoState.copy(hederaMemoState = HederaMemoState(input, memo)) - } - } - return updateLastState(sendState.copy(transactionExtrasState = result), result) - } - - private fun handleAlgorandMemo( - action: AlgorandMemo, - sendState: SendState, - infoState: TransactionExtrasState, - ): SendState { - val result = when (action) { - is AlgorandMemo.HandleUserInput -> { - val memo = action.data - val input = InputViewValue(memo, true) - infoState.copy(algorandMemoState = AlgorandMemoState(input, memo)) - } - } - return updateLastState(sendState.copy(transactionExtrasState = result), result) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressState.kt deleted file mode 100644 index 3310a772ce..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressState.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.tangem.tap.features.send.redux.states - -import androidx.core.text.isDigitsOnly -import com.tangem.blockchain.blockchains.stellar.StellarMemo -import com.tangem.tap.features.send.redux.AddressVerifyAction -import java.math.BigInteger - -data class AddressState( - val viewFieldValue: InputViewValue = InputViewValue(""), - val normalFieldValue: String? = null, - val truncatedFieldValue: String? = null, - val destinationWalletAddress: String? = null, - val error: AddressVerifyAction.Error? = null, - val truncateHandler: ((String) -> String)? = null, - val pasteIsEnabled: Boolean = false, - val inputIsEnabled: Boolean = true, -) : SendScreenState { - - override val stateId: StateId = StateId.ADDRESS_PAY_ID - - fun truncate(value: String): String = truncateHandler?.invoke(value) ?: value - - fun isReady(): Boolean = error == null && destinationWalletAddress?.isNotEmpty() ?: false -} - -@Deprecated("Legacy") -data class TransactionExtrasState( - val xlmMemo: XlmMemoState? = null, - val binanceMemo: BinanceMemoState? = null, - val xrpDestinationTag: XrpDestinationTagState? = null, - val tonMemoState: TonMemoState? = null, - val cosmosMemoState: CosmosMemoState? = null, - val hederaMemoState: HederaMemoState? = null, - val algorandMemoState: AlgorandMemoState? = null, -) : IdStateHolder { - override val stateId: StateId = StateId.TRANSACTION_EXTRAS - - fun isNull(): Boolean { - return xlmMemo == null && binanceMemo == null && xrpDestinationTag == null && tonMemoState == null && - cosmosMemoState == null && hederaMemoState == null && algorandMemoState == null - } - - fun isEmpty(): Boolean { - val isXlmEmpty = xlmMemo?.viewFieldValue?.value?.isEmpty() ?: false - val isBinanceEmpty = binanceMemo?.viewFieldValue?.value?.isEmpty() ?: false - val isXrpEmpty = xrpDestinationTag?.viewFieldValue?.value?.isEmpty() ?: false - val isTonEmpty = tonMemoState?.viewFieldValue?.value?.isEmpty() ?: false - val isCosmosEmpty = cosmosMemoState?.viewFieldValue?.value?.isEmpty() ?: false - val isHederaEmpty = hederaMemoState?.viewFieldValue?.value?.isEmpty() ?: false - val isAlgorandEmpty = algorandMemoState?.viewFieldValue?.value?.isEmpty() ?: false - - return isXlmEmpty || - isBinanceEmpty || - isXrpEmpty || - isTonEmpty || - isCosmosEmpty || - isHederaEmpty || - isAlgorandEmpty - } -} - -enum class XlmMemoType { - TEXT, ID -} - -data class XlmMemoState( - val viewFieldValue: InputViewValue = InputViewValue(""), - val text: StellarMemo.Text? = null, - val id: StellarMemo.Id? = null, - val error: TransactionExtraError? = null, -) { - val memo: StellarMemo? - get() = when (selectedMemoType) { - XlmMemoType.TEXT -> text - XlmMemoType.ID -> id - } - - val selectedMemoType: XlmMemoType - get() = determineMemoType(viewFieldValue.value) - - companion object { - fun determineMemoType(value: String): XlmMemoType = when { - value.isNotEmpty() && value.isDigitsOnly() -> XlmMemoType.ID - else -> XlmMemoType.TEXT - } - - @Suppress("MagicNumber") - fun isAssignableValue(value: String): Boolean = when (determineMemoType(value)) { - XlmMemoType.TEXT -> { - // from org.stellar.sdk.MemoText - value.toByteArray().size <= 28 - } - XlmMemoType.ID -> { - try { - // from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo - value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger() - } catch (ex: NumberFormatException) { - false - } - } - } - } -} - -data class BinanceMemoState( - val viewFieldValue: InputViewValue = InputViewValue(""), - val memo: BigInteger? = null, - val error: TransactionExtraError? = null, -) - -// tag must contains only digits -data class XrpDestinationTagState( - val viewFieldValue: InputViewValue = InputViewValue(""), - val tag: Long? = null, - val error: TransactionExtraError? = null, -) { - companion object { - const val MAX_NUMBER: Long = 4294967295 - } -} - -data class TonMemoState( - val viewFieldValue: InputViewValue = InputViewValue(""), - val memo: String? = null, - val error: TransactionExtraError? = null, -) - -data class CosmosMemoState( - val viewFieldValue: InputViewValue = InputViewValue(""), - val memo: String? = null, - val error: TransactionExtraError? = null, -) - -data class HederaMemoState( - val viewFieldValue: InputViewValue = InputViewValue(""), - val memo: String? = null, - val error: TransactionExtraError? = null, -) - -data class AlgorandMemoState( - val viewFieldValue: InputViewValue = InputViewValue(""), - val memo: String? = null, - val error: TransactionExtraError? = null, -) - -enum class TransactionExtraError { - INVALID_DESTINATION_TAG, - INVALID_XLM_MEMO, - INVALID_BINANCE_MEMO, -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt deleted file mode 100644 index 27ae5f8a33..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.tap.features.send.redux.states - -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.tap.common.entities.ProgressState -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -enum class FeeType { - SINGLE, LOW, NORMAL, PRIORITY; -} - -fun FeeType.convertToAnalyticsFeeType(): AnalyticsParam.FeeType { - return when (this) { - FeeType.SINGLE -> AnalyticsParam.FeeType.Fixed - FeeType.LOW -> AnalyticsParam.FeeType.Min - FeeType.NORMAL -> AnalyticsParam.FeeType.Normal - FeeType.PRIORITY -> AnalyticsParam.FeeType.Max - } -} - -data class FeeState( - val selectedFeeType: FeeType = FeeType.NORMAL, - val fees: TransactionFee? = null, - val currentFee: Fee? = null, - val feeIsIncluded: Boolean = false, - val feeIsApproximate: Boolean = false, - val mainLayoutIsVisible: Boolean = false, - val controlsLayoutIsVisible: Boolean = false, - val feeChipGroupIsVisible: Boolean = true, - val includeFeeSwitcherIsEnabled: Boolean = true, - val progressState: ProgressState = ProgressState.Done, -) : SendScreenState { - - override val stateId: StateId = StateId.FEE - - fun isReady(): Boolean = currentFee != null - - fun getCurrentFeeValue(): BigDecimal = currentFee?.amount?.value ?: BigDecimal.ZERO -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/ReceiptState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/ReceiptState.kt deleted file mode 100644 index f23d760f1a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/ReceiptState.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.tangem.tap.features.send.redux.states - -// Shows only one type of the layout -enum class ReceiptLayoutType { - UNKNOWN, - FIAT, - CRYPTO, - TOKEN_FIAT, - TOKEN_CRYPTO, - FEE_IN_CUSTOM_TOKEN, - FEE_IN_CUSTOM_TOKEN_FIAT, - SAME_CURRENCY, - SAME_CURRENCY_FIAT, -} - -data class ReceiptState( - val visibleTypeOfReceipt: ReceiptLayoutType? = null, - val fiat: ReceiptFiat? = null, - val crypto: ReceiptCrypto? = null, - val tokenFiat: ReceiptTokenFiat? = null, - val tokenCrypto: ReceiptTokenCrypto? = null, - val customTokenFiat: ReceiptTokenFiat? = null, - val customTokenCrypto: ReceiptTokenCrypto? = null, - val sameCurrencyFiat: ReceiptFiat? = null, - val sameCurrencyCrypto: ReceiptCrypto? = null, - val mainCurrency: MainCurrency? = null, -) : SendScreenState { - override val stateId: StateId = StateId.RECEIPT -} - -data class ReceiptSymbols( - val fiat: String, - val crypto: String, - val token: String? = null, - val fee: String? = null, -) - -data class ReceiptFiat( - val amountFiat: String, - val feeFiat: String, - val totalFiat: String, - val willSentCrypto: String, - val symbols: ReceiptSymbols, -) - -data class ReceiptCrypto( - val amountCrypto: String, - val feeCrypto: String, - val totalCrypto: String, - val feeFiat: String, - val willSentFiat: String, - val symbols: ReceiptSymbols, -) - -data class ReceiptTokenCrypto( - val amountToken: String, - val feeCoin: String, - val totalFiat: String, - val symbols: ReceiptSymbols, -) - -data class ReceiptTokenFiat( - val amountFiat: String, - val feeFiat: String, - val totalFiat: String, - val willSentToken: String, - val willSentFeeCoin: String, - val symbols: ReceiptSymbols, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt deleted file mode 100644 index 115ab92034..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt +++ /dev/null @@ -1,196 +0,0 @@ -package com.tangem.tap.features.send.redux.states - -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.WalletManager -import com.tangem.common.extensions.isZero -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.redux.StateDialog -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.tap.common.CurrencyConverter -import com.tangem.tap.common.entities.IndeterminateProgressButton -import com.tangem.tap.common.text.DecimalDigitsInputFilter -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.tap.store -import org.rekotlin.StateType -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ - -interface IdStateHolder { - val stateId: StateId -} - -enum class StateId { - SEND_SCREEN, ADDRESS_PAY_ID, TRANSACTION_EXTRAS, AMOUNT, FEE, RECEIPT -} - -interface SendScreenState : StateType, IdStateHolder - -data class SendState( - val walletManager: WalletManager? = null, - val currency: CryptoCurrency? = null, - val coinConverter: CurrencyConverter? = null, - val tokenConverter: CurrencyConverter? = null, - val customFeeConverter: CurrencyConverter? = null, - val lastChangedStates: LinkedHashSet = linkedSetOf(), - val addressState: AddressState = AddressState(), - val transactionExtrasState: TransactionExtrasState = TransactionExtrasState(), - val amountState: AmountState = AmountState(), - val feeState: FeeState = FeeState(), - val receiptState: ReceiptState = ReceiptState(), - val sendWarningsList: List = listOf(), - val sendButtonState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.DISABLED), - val dialog: StateDialog? = null, - val externalTransactionData: ExternalTransactionData? = null, - val isSuccessSend: Boolean = false, - val canIncludeFee: Boolean = false, -) : SendScreenState { - - override val stateId: StateId = StateId.SEND_SCREEN - - fun getDecimals(type: MainCurrencyType): Int = when (type) { - MainCurrencyType.FIAT -> 2 - MainCurrencyType.CRYPTO -> amountState.amountToExtract?.decimals ?: 0 - } - - private fun convertFiatToCoin(value: BigDecimal): BigDecimal { - return if (!this.coinIsConvertible()) value else coinConverter!!.toCrypto(value) - } - - private fun convertFiatToToken(value: BigDecimal): BigDecimal { - return if (!this.tokenIsConvertible()) value else tokenConverter!!.toCrypto(value) - } - - private fun convertCoinToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal { - if (!this.coinIsConvertible()) return value - - val converter = coinConverter!! - return if (!scaleWithPrecision) converter.toFiat(value) else converter.toFiatWithPrecision(value) - } - - private fun convertTokenToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal { - if (!this.tokenIsConvertible()) return value - - val converter = tokenConverter!! - return if (!scaleWithPrecision) converter.toFiat(value) else converter.toFiatWithPrecision(value) - } - - fun convertFiatToExtractCrypto(fiatValue: BigDecimal): BigDecimal = when (amountState.typeOfAmount) { - AmountType.Coin -> convertFiatToCoin(fiatValue) - is AmountType.Token -> convertFiatToToken(fiatValue) - is AmountType.FeeResource, - AmountType.Reserve, - -> fiatValue - } - - fun convertExtractCryptoToFiat(cryptoValue: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal { - return when (amountState.typeOfAmount) { - AmountType.Coin -> convertCoinToFiat(cryptoValue, scaleWithPrecision) - is AmountType.Token -> convertTokenToFiat(cryptoValue, scaleWithPrecision) - is AmountType.FeeResource, - AmountType.Reserve, - -> cryptoValue - } - } - - fun getButtonState(): ButtonState = if (isReadyToSend()) ButtonState.ENABLED else ButtonState.DISABLED - - fun getTotalAmountToSend(value: BigDecimal = amountState.amountToSendCrypto): BigDecimal { - val needToExtractFee = amountState.canIncludeFee() && feeState.feeIsIncluded - return if (needToExtractFee) value.minus(feeState.getCurrentFeeValue()) else value - } - - fun coinIsConvertible(): Boolean = coinConverter != null - fun tokenIsConvertible(): Boolean = tokenConverter != null - fun feeIsConvertible(): Boolean = customFeeConverter != null - - fun mainCurrencyCanBeSwitched(): Boolean { - return when (amountState.typeOfAmount) { - AmountType.Coin -> coinIsConvertible() - is AmountType.Token -> tokenIsConvertible() - is AmountType.FeeResource, - AmountType.Reserve, - -> false - } - } - - companion object { - private fun addressIsReady(): Boolean = store.state.sendState.addressState.isReady() - - private fun amountIsReady(): Boolean = store.state.sendState.amountState.isReady() - - fun isReadyToRequestFee(): Boolean = addressIsReady() && amountIsReady() - - fun isReadyToSend(): Boolean = addressIsReady() && amountIsReady() && - store.state.sendState.feeState.isReady() - } -} - -enum class ButtonState { - ENABLED, DISABLED, PROGRESS -} - -data class AmountState( - val amountToExtract: Amount? = null, - val typeOfAmount: AmountType = AmountType.Coin, - val viewAmountValue: InputViewValue = InputViewValue(BigDecimal.ZERO.toPlainString()), - val viewBalanceValue: String = BigDecimal.ZERO.toPlainString(), - val mainCurrency: MainCurrency = MainCurrency(MainCurrencyType.FIAT, AppCurrency.Default.code), - val amountToSendCrypto: BigDecimal = BigDecimal.ZERO, - val balanceCrypto: BigDecimal = BigDecimal.ZERO, - val hideBalance: Boolean = false, - val cursorAtTheSamePosition: Boolean = true, - val maxLengthOfAmount: Int = 2, - val decimalSeparator: String = ".", - val error: TapError? = null, - val inputIsEnabled: Boolean = true, -) : SendScreenState { - - override val stateId: StateId = StateId.AMOUNT - - fun isReady(): Boolean = error == null && !amountToSendCrypto.isZero() - - fun canIncludeFee(): Boolean = store.state.sendState.canIncludeFee - - fun createMainCurrency(type: MainCurrencyType, canSwitched: Boolean): MainCurrency { - return if (!canSwitched) { - MainCurrency(type, amountToExtract?.currencySymbol ?: "NONE", false) - } else { - when (type) { - MainCurrencyType.FIAT -> MainCurrency(type, store.state.globalState.appCurrency.code) - MainCurrencyType.CRYPTO -> MainCurrency(type, amountToExtract?.currencySymbol ?: "NONE") - } - } - } - - fun toBigDecimalSeparator(value: String): String { - return value.replace(",", ".") - } - - fun restoreDecimalSeparator(value: String): String { - return DecimalDigitsInputFilter.setDecimalSeparator(value, decimalSeparator) - } -} - -data class InputViewValue(val value: String, val isFromUserInput: Boolean = false) -enum class MainCurrencyType { - FIAT, CRYPTO -} - -data class MainCurrency( - val type: MainCurrencyType, - val currencySymbol: String, - val isEnabled: Boolean = true, -) - -data class ExternalTransactionData( - val amount: String, - val destinationAddress: String, - val transactionId: String, - val canAmountBeModified: Boolean = false, - val canDestinationAddressBeModified: Boolean = false, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/EditTextCustomPaste.kt b/app/src/main/java/com/tangem/tap/features/send/ui/EditTextCustomPaste.kt deleted file mode 100644 index d93147b24b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/ui/EditTextCustomPaste.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.tap.features.send.ui - -import android.R -import android.content.Context -import android.util.AttributeSet -import com.google.android.material.textfield.TextInputEditText - -class EditTextCustomPaste : TextInputEditText { - - private var onSystemPasteButtonClickListener: (() -> Unit)? = null - - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - - fun setOnSystemPasteButtonClickListener(callback: () -> Unit) { - onSystemPasteButtonClickListener = callback - } - - override fun onTextContextMenuItem(id: Int): Boolean { - val isConsumed = super.onTextContextMenuItem(id) - - if (id == R.id.paste) onSystemPasteButtonClickListener?.invoke() - - return isConsumed - } -} \ 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 deleted file mode 100644 index 34650ad800..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ /dev/null @@ -1,407 +0,0 @@ -@file:Suppress("MagicNumber") - -package com.tangem.tap.features.send.ui - -import android.content.Context -import android.os.Bundle -import android.text.method.DigitsKeyListener -import android.view.View -import android.view.inputmethod.EditorInfo -import android.widget.EditText -import androidx.core.view.postDelayed -import androidx.core.widget.addTextChangedListener -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import by.kirich1409.viewbindingdelegate.viewBinding -import com.google.android.material.textfield.TextInputEditText -import com.tangem.Message -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter -import com.tangem.core.analytics.Analytics -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.qrscanning.usecases.ParseQrCodeUseCase -import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.sdk.extensions.hideSoftKeyboard -import com.tangem.tap.common.KeyboardObserver -import com.tangem.tap.common.analytics.events.Token -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.getFromClipboard -import com.tangem.tap.common.extensions.setOnImeActionListener -import com.tangem.tap.common.recyclerView.SpaceItemDecoration -import com.tangem.tap.common.snackBar.MaxAmountSnackbar -import com.tangem.tap.common.text.truncateMiddleWith -import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget -import com.tangem.tap.common.toggleWidget.ViewStateWidget -import com.tangem.tap.features.BaseStoreFragment -import com.tangem.tap.features.addBackPressHandler -import com.tangem.tap.features.send.redux.* -import com.tangem.tap.features.send.redux.AddressActionUi.* -import com.tangem.tap.features.send.redux.AmountActionUi.* -import com.tangem.tap.features.send.redux.FeeActionUi.* -import com.tangem.tap.features.send.redux.states.FeeType -import com.tangem.tap.features.send.redux.states.MainCurrencyType -import com.tangem.tap.features.send.ui.adapters.WarningMessagesAdapter -import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber -import com.tangem.tap.store -import com.tangem.wallet.R -import com.tangem.wallet.databinding.FragmentSendBinding -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import java.text.DecimalFormatSymbols -import javax.inject.Inject - -private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L - -/** -[REDACTED_AUTHOR] - */ -@Suppress("LargeClass") -@OptIn(FlowPreview::class) -@AndroidEntryPoint -class SendFragment : BaseStoreFragment(R.layout.fragment_send) { - - private val viewModel by viewModels() - - lateinit var sendBtn: ViewStateWidget - - private lateinit var etAmountToSend: TextInputEditText - private lateinit var warningsAdapter: WarningMessagesAdapter - - private val sendSubscriber = SendStateSubscriber(this) - private lateinit var keyboardObserver: KeyboardObserver - - 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) - sendSubscriber.initViewModel(viewModel) - Analytics.send(Token.Send.ScreenOpened()) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - subscribeToTransactionExtrasFields() - subscribeToAddressField() - subscribeToAmountField() - } - } - - addBackPressHandler(this) - - etAmountToSend = view.findViewById(R.id.etAmountToSend) - - initSendButtonStates() - setupAddressLayout() - setupAmountLayout() - setupFeeLayout() - setupWarningMessages() - store.dispatch(SendActionUi.CheckIfTransactionDataWasProvided) - } - - private fun initSendButtonStates() = with(binding) { - btnSend.setOnClickListener { - store.dispatch( - SendActionUi.SendAmountToRecipient( - Message(getString(R.string.initial_message_sign_header)), - ), - ) - } - sendBtn = IndeterminateProgressButtonWidget(btnSend, progress) - } - - private fun setupAddressLayout() = with(binding.lSendAddress) { - store.dispatch(SetTruncateHandler { etAddress.truncateMiddleWith(it, "...") }) - store.dispatch(CheckClipboard(requireContext().getFromClipboard()?.toString())) - - etAddress.apply { - setOnSystemPasteButtonClickListener { - store.dispatch( - PasteAddress( - data = requireContext().getFromClipboard()?.toString() ?: "", - sourceType = Token.Send.AddressEntered.SourceType.PastePopup, - ), - ) - } - - setOnFocusChangeListener { _, hasFocus -> - store.dispatch(TruncateOrRestore(!hasFocus)) - } - } - - imvPaste.setOnClickListener { - Analytics.send(Token.Send.ButtonPaste()) - store.dispatch( - PasteAddress( - data = requireContext().getFromClipboard()?.toString() ?: "", - sourceType = Token.Send.AddressEntered.SourceType.PasteButton, - ), - ) - store.dispatch(TruncateOrRestore(!etAddress.isFocused)) - } - imvQrCode.setOnClickListener { - Analytics.send(Token.Send.ButtonQRCode()) - - store.dispatchNavigationAction { - push(AppRoute.QrScanning(source = SourceType.SEND)) - } - } - } - - private fun CoroutineScope.subscribeToAddressField() = with(binding.lSendAddress) { - etAddress.inputtedTextAsFlow() - .debounce(EDIT_TEXT_INPUT_DEBOUNCE) - .filter { store.state.sendState.addressState.viewFieldValue.value != it } - .onEach { - store.dispatch(AddressActionUi.HandleUserInput(it)) - } - .launchIn(this@subscribeToAddressField) - } - - private fun CoroutineScope.subscribeToAmountField() { - etAmountToSend.inputtedTextAsFlow() - .debounce(EDIT_TEXT_INPUT_DEBOUNCE) - .filter { store.state.sendState.amountState.viewAmountValue.value != it && it.isNotEmpty() } - .onEach { store.dispatch(AmountActionUi.HandleUserInput(it)) } - .launchIn(this) - } - - private fun CoroutineScope.subscribeToTransactionExtrasFields() = with(binding.lSendAddress) { - // TODO: [REDACTED_TASK_KEY] - etXlmMemo.inputtedTextAsFlow() - .debounce(EDIT_TEXT_INPUT_DEBOUNCE) - .filter { - val info = store.state.sendState.transactionExtrasState - info.xlmMemo?.viewFieldValue?.value != it - } - .onEach { store.dispatch(TransactionExtrasAction.XlmMemo.HandleUserInput(it)) } - .launchIn(this@subscribeToTransactionExtrasFields) - - etDestinationTag.inputtedTextAsFlow() - .debounce(EDIT_TEXT_INPUT_DEBOUNCE) - .filter { - val info = store.state.sendState.transactionExtrasState - info.xrpDestinationTag?.viewFieldValue?.value != it - } - .onEach { store.dispatch(TransactionExtrasAction.XrpDestinationTag.HandleUserInput(it)) } - .launchIn(this@subscribeToTransactionExtrasFields) - - etBinanceMemo.inputtedTextAsFlow() - .debounce(EDIT_TEXT_INPUT_DEBOUNCE) - .filter { - val info = store.state.sendState.transactionExtrasState - info.binanceMemo?.viewFieldValue?.value != it - } - .onEach { store.dispatch(TransactionExtrasAction.BinanceMemo.HandleUserInput(it)) } - .launchIn(this@subscribeToTransactionExtrasFields) - - etTonMemo.inputtedTextAsFlow() - .debounce(EDIT_TEXT_INPUT_DEBOUNCE) - .filter { - val info = store.state.sendState.transactionExtrasState - info.tonMemoState?.viewFieldValue?.value != it - } - .onEach { store.dispatch(TransactionExtrasAction.TonMemo.HandleUserInput(it)) } - .launchIn(this@subscribeToTransactionExtrasFields) - - etCosmosMemo.inputtedTextAsFlow() - .debounce(EDIT_TEXT_INPUT_DEBOUNCE) - .filter { - val info = store.state.sendState.transactionExtrasState - info.cosmosMemoState?.viewFieldValue?.value != it - } - .onEach { store.dispatch(TransactionExtrasAction.CosmosMemo.HandleUserInput(it)) } - .launchIn(this@subscribeToTransactionExtrasFields) - - etHederaMemo.inputtedTextAsFlow() - .debounce(EDIT_TEXT_INPUT_DEBOUNCE) - .filter { - val info = store.state.sendState.transactionExtrasState - info.hederaMemoState?.viewFieldValue?.value != it - } - .onEach { store.dispatch(TransactionExtrasAction.HederaMemo.HandleUserInput(it)) } - .launchIn(this@subscribeToTransactionExtrasFields) - - etAlgorandMemo.inputtedTextAsFlow() - .debounce(EDIT_TEXT_INPUT_DEBOUNCE) - .filter { - val info = store.state.sendState.transactionExtrasState - info.algorandMemoState?.viewFieldValue?.value != it - } - .onEach { store.dispatch(TransactionExtrasAction.AlgorandMemo.HandleUserInput(it)) } - .launchIn(this@subscribeToTransactionExtrasFields) - } - - private fun setupAmountLayout() { - store.dispatch(SetMainCurrency(restoreMainCurrency())) - store.dispatch(ReceiptAction.RefreshReceipt) - store.dispatch(SendAction.ChangeSendButtonState(store.state.sendState.getButtonState())) - - val decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator.toString() - store.dispatch(AmountAction.SetDecimalSeparator(decimalSeparator)) - - binding.lSendAmount.tvAmountCurrency.setOnClickListener { - Analytics.send(Token.Send.ButtonSwapCurrency()) - store.dispatch(ToggleMainCurrency) - store.dispatch(ReceiptAction.RefreshReceipt) - store.dispatch(SendAction.ChangeSendButtonState(store.state.sendState.getButtonState())) - } - - val maxAmountSnackbar = MaxAmountSnackbar.make(etAmountToSend) { - etAmountToSend.clearFocus() - etAmountToSend.postDelayed(200) { etAmountToSend.hideSoftKeyboard() } - store.dispatch(SetMaxAmount) - } - var snackbarControlledByChangingFocus = false - keyboardObserver = KeyboardObserver(requireActivity()) - keyboardObserver.registerListener { isShow -> - if (snackbarControlledByChangingFocus) return@registerListener - - if (isShow) { - if (etAmountToSend.isFocused && !maxAmountSnackbar.isShown) maxAmountSnackbar.show() - } else { - if (maxAmountSnackbar.isShown) maxAmountSnackbar.dismiss() - } - } - - etAmountToSend.keyListener = DigitsKeyListener.getInstance("0123456789,.") - etAmountToSend.setOnFocusChangeListener { _, hasFocus -> - snackbarControlledByChangingFocus = true - if (hasFocus) { - etAmountToSend.postDelayed(200) { - maxAmountSnackbar.show() - snackbarControlledByChangingFocus = false - } - } else { - etAmountToSend.postDelayed(350) { - maxAmountSnackbar.dismiss() - snackbarControlledByChangingFocus = false - } - } - } - - val prevFocusChangeListener = etAmountToSend.onFocusChangeListener - etAmountToSend.setOnFocusChangeListener { v, hasFocus -> - prevFocusChangeListener.onFocusChange(v, hasFocus) - if (hasFocus && etAmountToSend.text?.toString() == "0") etAmountToSend.setText("") - if (!hasFocus && etAmountToSend.text?.toString() == "") etAmountToSend.setText("0") - } - - etAmountToSend.setOnImeActionListener(EditorInfo.IME_ACTION_DONE) { - it.hideSoftKeyboard() - it.clearFocus() - } - } - - private fun setupFeeLayout() = with(binding.clNetworkFee) { - flExpandCollapse.flExpandCollapse.setOnClickListener { - store.dispatch(ToggleControlsVisibility) - } - chipGroup.check(FeeUiHelper.toId(FeeType.NORMAL)) - chipGroup.setOnCheckedChangeListener { _, checkedId -> - if (checkedId == -1) return@setOnCheckedChangeListener - - store.dispatch(ChangeSelectedFee(FeeUiHelper.toType(checkedId))) - store.dispatch(CheckAmountToSend) - } - swIncludeFee.setOnCheckedChangeListener { _, isChecked -> - store.dispatch(ChangeIncludeFee(isChecked)) - store.dispatch(CheckAmountToSend) - } - } - - private fun setupWarningMessages() = with(binding) { - warningsAdapter = WarningMessagesAdapter() - val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - rvWarningMessages.layoutManager = layoutManager - rvWarningMessages.addItemDecoration(SpaceItemDecoration.all(16f)) - rvWarningMessages.adapter = warningsAdapter - - store.dispatch(SendAction.Warnings.Update) - } - - override fun subscribeToStore() { - store.subscribe(sendSubscriber) { appState -> - appState.skipRepeats { oldState, newState -> - oldState.sendState == newState.sendState - }.select { it.sendState } - } - storeSubscribersList.add(sendSubscriber) - } - - private fun restoreMainCurrency(): MainCurrencyType { - val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE) - val mainCurrency = sp.getString("mainCurrency", AppCurrency.Default.code) - return MainCurrencyType.values() - .firstOrNull { it.name.equals(mainCurrency!!, ignoreCase = true) } - ?: MainCurrencyType.CRYPTO - } - - fun saveMainCurrency(type: MainCurrencyType) { - val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE) - sp.edit().putString("mainCurrency", type.name).apply() - } - - override fun handleOnBackPressed() { - val externalTransactionData = store.state.sendState.externalTransactionData - if (externalTransactionData == null) { - store.dispatchNavigationAction(AppRouter::pop) - } else { - store.dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)) - } - } - - override fun onDestroyView() { - keyboardObserver.unregisterListener() - super.onDestroyView() - } - - override fun onDestroy() { - store.dispatch(ReleaseSendState) - lifecycle.removeObserver(viewModel) - super.onDestroy() - } -} - -fun EditText.inputtedTextAsFlow(): Flow = callbackFlow { - val watcher = addTextChangedListener { editable -> trySend(editable?.toString() ?: "") } - awaitClose { removeTextChangedListener(watcher) } -} - -object FeeUiHelper { - fun toId(fee: FeeType): Int { - return when (fee) { - FeeType.SINGLE -> View.NO_ID - FeeType.LOW -> R.id.chipLow - FeeType.NORMAL -> R.id.chipNormal - FeeType.PRIORITY -> R.id.chipPriority - } - } - - fun toType(id: Int): FeeType { - return when (id) { - R.id.chipLow -> FeeType.LOW - R.id.chipNormal -> FeeType.NORMAL - R.id.chipPriority -> FeeType.PRIORITY - else -> FeeType.NORMAL - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt deleted file mode 100644 index 881e9f6df2..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.tangem.tap.features.send.ui - -import androidx.lifecycle.* -import arrow.core.getOrElse -import com.tangem.common.routing.AppRoute -import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -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.FetchPendingTransactionsUseCase -import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.common.analytics.events.Token -import com.tangem.tap.di.DelayedWork -import com.tangem.tap.features.send.redux.AddressActionUi -import com.tangem.tap.features.send.redux.AmountAction -import com.tangem.tap.proxy.AppStateHolder -import com.tangem.tap.store -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@Suppress("LongParameterList") -@HiltViewModel -internal class SendViewModel @Inject constructor( - private val dispatchers: CoroutineDispatcherProvider, - private val appStateHolder: AppStateHolder, - private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, - private val listenToQrScanningUseCase: ListenToQrScanningUseCase, - private val parseQrCodeUseCase: ParseQrCodeUseCase, - @DelayedWork private val coroutineScope: CoroutineScope, - savedStateHandle: SavedStateHandle, -) : ViewModel(), DefaultLifecycleObserver { - - init { - listenToQrScanningUseCase(SourceType.SEND) - .getOrElse { emptyFlow() } - .onEach(::onQRCodeScanned) - .launchIn(viewModelScope) - } - - private val cryptoCurrency: CryptoCurrency? = savedStateHandle[AppRoute.Send.CRYPTO_CURRENCY_KEY] - - override fun onCreate(owner: LifecycleOwner) { - getBalanceHidingSettingsUseCase() - .flowWithLifecycle(owner.lifecycle) - .onEach { - appStateHolder.mainStore?.dispatch(AmountAction.HideBalance(it.isBalanceHidden)) - } - .flowOn(dispatchers.main) - .launchIn(viewModelScope) - } - - fun updateCurrencyDelayed() { - if (cryptoCurrency != null) { - coroutineScope.launch { - getSelectedWalletSyncUseCase() - .fold( - ifLeft = { Timber.e(it.toString()) }, - ifRight = { wallet -> - // we should update network to find pending tx after 1 sec - updateForPendingTx(wallet, cryptoCurrency.network) - // we should update network for new balance - updateForBalance(wallet, cryptoCurrency.network) - }, - ) - } - } else { - Timber.w("$TAG: cryptoCurrency is null, legacy flow") - } - } - - private suspend fun updateForPendingTx(userWallet: UserWallet, network: Network) { - fetchPendingTransactionsUseCase(userWallet.walletId, setOf(network)) - } - - private suspend fun updateForBalance(userWallet: UserWallet, network: Network) { - updateDelayedCurrencyStatusUseCase( - userWalletId = userWallet.walletId, - network = network, - delayMillis = UPDATE_BALANCE_DELAY_MILLIS, - refresh = true, - ) - } - - private fun onQRCodeScanned(qrScanResult: String) { - if (cryptoCurrency != null) { - parseQrCodeUseCase(qrScanResult, cryptoCurrency).fold( - ifRight = { parsedCode -> - store.dispatch( - AddressActionUi.PasteAddress( - data = parsedCode.address, - sourceType = Token.Send.AddressEntered.SourceType.QRCode, - ), - ) - parsedCode.amount?.let { amount -> - store.dispatch(AmountAction.SetAmount(amount, isUserInput = false)) - } - // parsedCode.memo?.let { } - }, - ifLeft = { - store.dispatch( - AddressActionUi.PasteAddress( - data = qrScanResult, - sourceType = Token.Send.AddressEntered.SourceType.QRCode, - ), - ) - Timber.w(it) - }, - ) - } else { - store.dispatch( - AddressActionUi.PasteAddress( - data = qrScanResult, - sourceType = Token.Send.AddressEntered.SourceType.QRCode, - ), - ) - } - - store.dispatch(AddressActionUi.TruncateOrRestore(truncate = true)) - } - - companion object { - private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L - private const val TAG = "SendViewModel" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/adapters/WarningMessagesAdapter.kt b/app/src/main/java/com/tangem/tap/features/send/ui/adapters/WarningMessagesAdapter.kt deleted file mode 100644 index 2703f44c00..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/ui/adapters/WarningMessagesAdapter.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.tangem.tap.features.send.ui.adapters - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.google.android.play.core.review.ReviewManagerFactory -import com.tangem.core.analytics.Analytics -import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.analytics.events.MainScreen -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.tap.store -import com.tangem.wallet.R -import com.tangem.wallet.databinding.LayoutWarningCardActionBinding -import timber.log.Timber - -// TODO: Delete with SendFeatureToggles -@Deprecated(message = "Used only in old send screen") -class WarningMessagesAdapter : ListAdapter(DiffUtilCallback) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WarningMessageVH { - val binding = LayoutWarningCardActionBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false, - ) - return WarningMessageVH(binding) - } - - override fun onBindViewHolder(holder: WarningMessageVH, position: Int) { - holder.bind(currentList[position]) - } - - object DiffUtilCallback : DiffUtil.ItemCallback() { - override fun areContentsTheSame(oldItem: WarningMessage, newItem: WarningMessage) = oldItem == newItem - - override fun areItemsTheSame(oldItem: WarningMessage, newItem: WarningMessage) = oldItem == newItem - } -} - -class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerView.ViewHolder(binding.root) { - - fun bind(warning: WarningMessage) { - setBgColor(warning.priority) - setText(warning) - setupControlButtons(warning) - } - - private fun setText(warning: WarningMessage) = with(binding.warningContentContainer) { - fun getString(resId: Int?, default: String, formatArgs: String? = null) = - if (resId == null) default else root.getString(resId, formatArgs) - - tvTitle.text = getString( - resId = warning.titleResId, - default = warning.title, - formatArgs = warning.titleFormatArg, - ) - - tvMessage.text = getString( - resId = warning.messageResId, - default = warning.message, - formatArgs = warning.messageFormatArg, - ) - } - - private fun setBgColor(priority: WarningMessage.Priority) { - val color = when (priority) { - WarningMessage.Priority.Info -> R.color.warning_info - WarningMessage.Priority.Warning -> R.color.warning_warning - WarningMessage.Priority.Critical -> R.color.warning_critical - } - binding.warningCardAction.setCardBackgroundColor(binding.root.getColor(color)) - } - - @Suppress("LongMethod") - private fun setupControlButtons(warning: WarningMessage) = when (warning.type) { - WarningMessage.Type.Permanent, WarningMessage.Type.TestCard -> { - binding.groupControlsTemporary.hide() - binding.btnClose.hide() - } - WarningMessage.Type.Temporary -> { - binding.groupControlsTemporary.show() - binding.btnClose.hide() - - val buttonAction = View.OnClickListener { - store.dispatch(GlobalAction.HideWarningMessage(warning)) - } - val buttonTitle = binding.root.getString( - warning.buttonTextId ?: R.string.how_to_got_it_button, - ) - binding.btnGotIt.setOnClickListener(buttonAction) - binding.btnGotIt.text = buttonTitle - } - WarningMessage.Type.AppRating -> { - binding.groupControlsTemporary.hide() - binding.btnClose.show() - binding.btnClose.setOnClickListener { - Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Closed)) - store.dispatch(GlobalAction.HideWarningMessage(warning)) - } - binding.btnReallyCool.setOnClickListener { - val activity = binding.root.context.getActivity() ?: return@setOnClickListener - - Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Liked)) - val reviewManager = ReviewManagerFactory.create(activity) - val task = reviewManager.requestReviewFlow() - task.addOnCompleteListener { - if (!it.isSuccessful) { - Timber.e(task.exception) - } - }.addOnFailureListener { - Timber.e(it) - } - store.dispatch(GlobalAction.HideWarningMessage(warning)) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/ChiaWarningDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/ChiaWarningDialog.kt deleted file mode 100644 index b4ba056146..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/ChiaWarningDialog.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.tap.features.send.ui.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.tangem.tap.features.send.redux.SendAction -import com.tangem.tap.store -import com.tangem.wallet.R - -// TODO: [REDACTED_JIRA] -object ChiaWarningDialog { - - fun create(context: Context, dialog: SendAction.Dialog.ChiaWarningDialog): AlertDialog { - return AlertDialog.Builder(context).apply { - setTitle(R.string.common_warning) - setMessage( - context.getString( - R.string.common_utxo_validate_withdrawal_message_warning, - dialog.blockchainName, - dialog.maxOutputs, - dialog.maxAmount.toPlainString(), - ), - ) - setPositiveButton(R.string.common_ok) { _, _ -> - dialog.onOk() - } - setOnDismissListener { - store.dispatch(SendAction.Dialog.Hide) - } - } - .create() - } -} \ No newline at end of file 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 deleted file mode 100644 index a86e233c8b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt +++ /dev/null @@ -1,31 +0,0 @@ -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.common_utxo_validate_withdrawal_message_warning, - Blockchain.Kaspa.fullName, - dialog.maxOutputs, - dialog.maxAmount.toPlainString(), - ), - ) - setPositiveButton(R.string.common_ok) { _, _ -> - dialog.onOk() - } - setOnDismissListener { - store.dispatch(SendAction.Dialog.Hide) - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt deleted file mode 100644 index bb8a5ea183..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.tap.features.send.ui.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.tap.common.feedback.SendTransactionFailedEmail -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.send.redux.SendAction -import com.tangem.tap.store -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -object RequestFeeErrorDialog { - fun create(context: Context, dialog: SendAction.Dialog.RequestFeeError): AlertDialog { - val errorMessage = dialog.error.customMessage - - return AlertDialog.Builder(context).apply { - setTitle(R.string.common_fee_error) - setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage)) - setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ -> - Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send)) - store.dispatch( - GlobalAction.SendEmail( - feedbackData = SendTransactionFailedEmail(errorMessage), - scanResponse = dialog.scanResponse, - ), - ) - } - setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() } - setNeutralButton(R.string.common_cancel) { _, _ -> } - setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt deleted file mode 100644 index a768ea033b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt +++ /dev/null @@ -1,65 +0,0 @@ -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.blockchain.common.BlockchainSdkError -import com.tangem.common.module.ModuleMessageConverter -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.sdk.extensions.localizedDescription -import com.tangem.tap.common.extensions.stripZeroPlainString -import com.tangem.tap.common.feedback.SendTransactionFailedEmail -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.send.redux.SendAction -import com.tangem.tap.store -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -object SendTransactionFailsDialog { - fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.CardSdkError): AlertDialog { - return create(context, dialog.error.localizedDescription(context), dialog.scanResponse) - } - - fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.BlockchainSdkError): AlertDialog { - val errorConverter = BlockchainSdkErrorConverter(context) - return create(context, errorConverter.convert(dialog.error), dialog.scanResponse) - } - - private fun create(context: Context, errorMessage: String, scanResponse: ScanResponse): AlertDialog { - return AlertDialog.Builder(context).apply { - setTitle(R.string.alert_failed_to_send_transaction_title) - setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage)) - setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ -> - Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send)) - store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage), scanResponse)) - } - setPositiveButton(R.string.common_cancel) { _, _ -> } - setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) } - }.create() - } -} - -private class BlockchainSdkErrorConverter( - private val context: Context, -) : ModuleMessageConverter { - - override fun convert(message: BlockchainSdkError): String { - return when (message) { - is BlockchainSdkError.CreateAccountUnderfunded -> { - val resStringId = when (message.blockchain) { - Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.string.no_account_polkadot - else -> R.string.send_error_no_target_account - } - val reserveValueString = message.minReserve.value?.stripZeroPlainString() ?: "0" - val argument = "$reserveValueString ${message.minReserve.currencySymbol}" - context.getString(resStringId, argument) - } - else -> message.customMessage - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/TezosWarningDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/TezosWarningDialog.kt deleted file mode 100644 index afb6276cb7..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/TezosWarningDialog.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.tap.features.send.ui.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.tangem.tap.features.send.redux.SendAction -import com.tangem.tap.store -import com.tangem.wallet.R - -class TezosWarningDialog(context: Context) : AlertDialog(context) { - - companion object { - fun create(context: Context, showDialogData: SendAction.Dialog.TezosWarningDialog): AlertDialog { - val reduceAmount = showDialogData.reduceAmount.toPlainString() - return Builder(context).apply { - setTitle(R.string.common_warning) - setMessage(context.getString(R.string.xtz_withdrawal_message_warning, reduceAmount)) - setNegativeButton(R.string.xtz_withdrawal_message_ignore) { _, _ -> - showDialogData.sendAllCallback() - } - setPositiveButton(context.getString(R.string.xtz_withdrawal_message_reduce, reduceAmount)) { _, _ -> - showDialogData.reduceCallback() - } - setOnDismissListener { - store.dispatch(SendAction.Dialog.Hide) - } - }.create() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/FragmentStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/FragmentStateSubscriber.kt deleted file mode 100644 index 17dfb7a5fc..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/FragmentStateSubscriber.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.features.send.ui.stateSubscribers - -import com.tangem.tap.features.BaseStoreFragment -import org.rekotlin.StateType -import org.rekotlin.StoreSubscriber -import java.lang.ref.WeakReference - -/** -[REDACTED_AUTHOR] - */ -abstract class FragmentStateSubscriber(fragment: BaseStoreFragment) : StoreSubscriber { - private val weakFragment: WeakReference = WeakReference(fragment) - - abstract fun updateWithNewState(fg: BaseStoreFragment, state: S) - - override fun newState(state: S) { - val fg = weakFragment.get() ?: return - - updateWithNewState(fg, state) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt deleted file mode 100644 index 81899d8e65..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ /dev/null @@ -1,530 +0,0 @@ -package com.tangem.tap.features.send.ui.stateSubscribers - -import android.app.Dialog -import android.content.Context -import android.text.SpannableStringBuilder -import android.view.View -import android.view.ViewGroup -import androidx.core.text.bold -import com.tangem.common.extensions.remove -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.tap.common.entities.ProgressState -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.redux.getMessageString -import com.tangem.tap.common.text.DecimalDigitsInputFilter -import com.tangem.tap.domain.MultiMessageError -import com.tangem.tap.domain.assembleErrors -import com.tangem.tap.features.BaseStoreFragment -import com.tangem.tap.features.send.redux.AddressVerifyAction.Error -import com.tangem.tap.features.send.redux.SendAction -import com.tangem.tap.features.send.redux.states.* -import com.tangem.tap.features.send.ui.FeeUiHelper -import com.tangem.tap.features.send.ui.SendFragment -import com.tangem.tap.features.send.ui.SendViewModel -import com.tangem.tap.features.send.ui.adapters.WarningMessagesAdapter -import com.tangem.tap.features.send.ui.dialogs.* -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Suppress("LargeClass") -internal class SendStateSubscriber( - fragment: BaseStoreFragment, -) : FragmentStateSubscriber(fragment) { - - private var dialog: Dialog? = null - private var sendViewModel: SendViewModel? = null - fun initViewModel(viewModel: SendViewModel) { - sendViewModel = viewModel - } - - override fun updateWithNewState(fg: BaseStoreFragment, state: SendState) { - fg.view ?: return - if (fg !is SendFragment) return - if (state.isSuccessSend) { - sendViewModel?.updateCurrencyDelayed() - return - } - - val lastChangedStates = state.lastChangedStates.toList() - state.lastChangedStates.clear() - (fg as? SendFragment)?.binding?.mainSendContainer?.beginDelayedTransition() - lastChangedStates.forEach { - when (it) { - StateId.SEND_SCREEN -> handleSendScreen(fg, state) - StateId.ADDRESS_PAY_ID -> handleAddressState(fg, state.addressState) - StateId.TRANSACTION_EXTRAS -> handleTransactionExtrasState(fg, state.transactionExtrasState) - StateId.AMOUNT -> handleAmountState(fg, state.amountState) - StateId.FEE -> handleFeeState(fg, state.feeState) - StateId.RECEIPT -> handleReceiptState(fg, state.receiptState, state.feeState.progressState) - } - } - } - - @Deprecated("Legacy") - @Suppress("ComplexMethod", "LongMethod") - private fun handleTransactionExtrasState(fg: SendFragment, infoState: TransactionExtrasState) = - with(fg.binding.lSendAddress) { - fun showView(view: View, info: Any?) { - view.show(info != null) - } - showView(xlmMemoContainer, infoState.xlmMemo) - showView(xrpDestinationTagContainer, infoState.xrpDestinationTag) - showView(binanceMemoContainer, infoState.binanceMemo) - showView(tonMemoContainer, infoState.tonMemoState) - showView(cosmosMemoContainer, infoState.cosmosMemoState) - showView(hederaMemoContainer, infoState.hederaMemoState) - showView(algorandMemoContainer, infoState.algorandMemoState) - - infoState.xlmMemo?.let { - if (!it.viewFieldValue.isFromUserInput) etXlmMemo.setText(it.viewFieldValue.value) - if (it.error != null) { - if (it.error == TransactionExtraError.INVALID_XLM_MEMO) { - tilXlmMemo.error = fg.getText(R.string.send_extras_error_invalid_memo) - } - } else { - tilXlmMemo.error = null - } - } - infoState.xrpDestinationTag?.let { - if (infoState.xrpDestinationTag.error != null) { - if (infoState.xrpDestinationTag.error == TransactionExtraError.INVALID_DESTINATION_TAG) { - tilDestinationTag.error = - fg.getText(R.string.send_extras_error_invalid_destination_tag) - } - } else { - tilDestinationTag.error = null - } - if (!it.viewFieldValue.isFromUserInput) { - etDestinationTag.setText(it.viewFieldValue.value) - } - } - infoState.binanceMemo?.let { - if (infoState.binanceMemo.error != null) { - if (infoState.binanceMemo.error == TransactionExtraError.INVALID_BINANCE_MEMO) { - tilBinanceMemo.error = fg.getText(R.string.send_extras_error_invalid_memo) - } - } else { - tilBinanceMemo.error = null - } - if (!it.viewFieldValue.isFromUserInput) { - etBinanceMemo.setText(it.viewFieldValue.value) - } - } - infoState.tonMemoState?.let { - if (infoState.tonMemoState.error != null) { - tilTonMemo.error = fg.getText(R.string.send_extras_error_invalid_memo) - } else { - tilBinanceMemo.error = null - } - if (!it.viewFieldValue.isFromUserInput) { - etTonMemo.setText(it.viewFieldValue.value) - } - } - infoState.cosmosMemoState?.let { - if (infoState.cosmosMemoState.error != null) { - tilCosmosMemo.error = fg.getText(R.string.send_extras_error_invalid_memo) - } else { - tilBinanceMemo.error = null - } - if (!it.viewFieldValue.isFromUserInput) { - etCosmosMemo.setText(it.viewFieldValue.value) - } - } - infoState.hederaMemoState?.let { - if (infoState.hederaMemoState.error != null) { - tilHederaMemo.error = fg.getText(R.string.send_extras_error_invalid_memo) - } else { - tilHederaMemo.error = null - } - if (!it.viewFieldValue.isFromUserInput) { - etHederaMemo.setText(it.viewFieldValue.value) - } - } - infoState.algorandMemoState?.let { - if (infoState.algorandMemoState.error != null) { - tilAlgorandMemo.error = fg.getText(R.string.send_extras_error_invalid_memo) - } else { - tilAlgorandMemo.error = null - } - if (!it.viewFieldValue.isFromUserInput) { - etAlgorandMemo.setText(it.viewFieldValue.value) - } - } - } - - @Suppress("ComplexMethod") - private fun handleSendScreen(fg: SendFragment, state: SendState) = with(fg.binding) { - when (state.dialog) { - is SendAction.Dialog.TezosWarningDialog -> { - if (dialog == null) { - dialog = TezosWarningDialog.create(fg.requireContext(), state.dialog) - dialog?.show() - } - } - is SendAction.Dialog.KaspaWarningDialog -> { - if (dialog == null) { - dialog = KaspaWarningDialog.create(fg.requireContext(), state.dialog) - dialog?.show() - } - } - is SendAction.Dialog.ChiaWarningDialog -> { - if (dialog == null) { - dialog = ChiaWarningDialog.create(fg.requireContext(), state.dialog) - dialog?.show() - } - } - is SendAction.Dialog.SendTransactionFails.CardSdkError -> { - if (dialog == null) { - dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog) - dialog?.show() - } - } - is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> { - if (dialog == null) { - dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog) - dialog?.show() - } - } - is SendAction.Dialog.RequestFeeError -> { - if (dialog == null) { - dialog = RequestFeeErrorDialog.create(fg.requireContext(), state.dialog) - dialog?.show() - } - } - else -> { - dialog?.dismiss() - dialog = null - } - } - - fg.sendBtn.changeState(state.sendButtonState.progressState) - fg.sendBtn.mainView.isEnabled = state.sendButtonState.enabled - - val rv = rvWarningMessages - val adapter = rv.adapter as? WarningMessagesAdapter ?: return - - adapter.submitList(state.sendWarningsList) - rv.show(state.sendWarningsList.isNotEmpty()) - - toolbar.title = fg.getString( - R.string.send_title_currency_format, - state.amountState.mainCurrency.currencySymbol, - ) - } - - private fun handleAddressState(fg: SendFragment, state: AddressState) { - with(fg.binding.lSendAddress) { - fun parseError(context: Context, error: Error?): String? { - val resId = when (error) { - Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_validation_invalid_address - Error.ADDRESS_SAME_AS_WALLET -> R.string.send_error_address_same_as_wallet - else -> null - } - return if (resId == null) null else context.getString(resId, "", "") - } - - imvPaste.isEnabled = state.pasteIsEnabled - - val et = etAddress - val til = tilAddress - val parsedError = parseError(til.context, state.error) - - til.isEnabled = state.inputIsEnabled - imvPaste.show(state.inputIsEnabled) - imvQrCode.show(state.inputIsEnabled) - flPaste.show(state.inputIsEnabled) - flQrCode.show(state.inputIsEnabled) - - til.hint = til.getString(R.string.send_destination_hint_address) - til.error = parsedError - til.isErrorEnabled = parsedError != null - til.helperText = state.destinationWalletAddress - til.isHelperTextEnabled = parsedError == null - - if (!state.viewFieldValue.isFromUserInput) et.update(state.viewFieldValue.value) - } - } - - private fun handleAmountState(fg: SendFragment, state: AmountState) = with(fg.binding.lSendAmount) { - if (state.error != null) { - val context = fg.requireContext() - val message = when (state.error) { - is MultiMessageError -> { - val multiError = state.error as MultiMessageError - val messageList = multiError.assembleErrors() - .map { getMessageString(context, it.first, it.second) } - multiError.builder(messageList) - } - else -> context.getString(state.error.messageResource) - } - tilAmountToSend.enableError(true, message) - } else { - tilAmountToSend.enableError(false) - } - - val filter = DecimalDigitsInputFilter( - digitsBeforeDecimal = 40, - digitsAfterDecimal = state.maxLengthOfAmount, - decimalSeparator = state.decimalSeparator, - ) - etAmountToSend.filters = arrayOf(filter) - val amountToSend = state.viewAmountValue - if (!amountToSend.isFromUserInput) etAmountToSend.update(amountToSend.value) - - tvAmountCurrency.update(state.mainCurrency.currencySymbol) - (fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.type) - - val balanceText = when (state.mainCurrency.type) { - MainCurrencyType.FIAT -> fg.getString( - R.string.common_balance, - "${state.viewBalanceValue} ${state.mainCurrency.currencySymbol}", - ).remove(":") - MainCurrencyType.CRYPTO -> fg.getString( - R.string.common_balance, - "${state.mainCurrency.currencySymbol} ${state.viewBalanceValue}", - ) - } - - tvBalance.update(balanceText) - - tilAmountToSend.isEnabled = state.inputIsEnabled - - val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0 - tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0) - val textColor = if (state.inputIsEnabled) R.color.accent else R.color.text_secondary - tvAmountCurrency.setTextColor(fg.getColor(textColor)) - } - - @Suppress("MagicNumber") - private fun handleFeeState(fg: SendFragment, state: FeeState) { - with(fg.binding.clNetworkFee) { - fg.view?.findViewById(R.id.clNetworkFee)?.show(state.mainLayoutIsVisible) - flExpandCollapse.imvExpandCollapse.rotation = if (state.controlsLayoutIsVisible) 0f else 180f - llFeeControlsContainer.show(state.controlsLayoutIsVisible) - chipGroup.show(state.feeChipGroupIsVisible) - - swIncludeFee.isEnabled = state.includeFeeSwitcherIsEnabled - if (swIncludeFee.isChecked != state.feeIsIncluded) { - swIncludeFee.isChecked = state.feeIsIncluded - } - - val chipId = FeeUiHelper.toId(state.selectedFeeType) - if (chipGroup.checkedChipId != chipId && chipId != View.NO_ID) chipGroup.check(chipId) - } - with(fg.binding.clReceiptContainer) { - when (state.progressState) { - ProgressState.Loading -> { - tvReceiptFeeValue.hide() - pbReceiptFee.show() - } - ProgressState.Done -> { - pbReceiptFee.hide() - tvReceiptFeeValue.show() - } - else -> {} - } - } - } - - @Suppress("LongMethod", "ComplexMethod") - private fun handleReceiptState(fg: SendFragment, state: ReceiptState, feeProgressState: ProgressState) { - with(fg.binding.clReceiptContainer) { - val mainLayout = clReceiptContainer as ViewGroup - val totalLayout = llTotalContainer.llTotal as ViewGroup - val totalTokenLayout = llTotalContainer.flTotalTokenCrypto as ViewGroup - - fun getString(id: Int, vararg formatStrings: String): String = mainLayout.getString(id, *formatStrings) - - fun roughOrEmpty(value: String): String { - return if (value == BigDecimalFormatter.EMPTY_BALANCE_SIGN) value else "$ROUGH_SIGN $value" - } - - when (feeProgressState) { - ProgressState.Loading -> { - tvReceiptFeeValue.hide() - pbReceiptFee.show() - } - ProgressState.Done -> { - pbReceiptFee.hide() - tvReceiptFeeValue.show() - } - else -> {} - } - - when (state.visibleTypeOfReceipt) { - ReceiptLayoutType.FIAT -> { - val receipt = state.fiat ?: return - - totalLayout.show(true) - totalTokenLayout.show(false) - tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}") - tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}") - llTotalContainer.tvTotalValue.post { - llTotalContainer.tvTotalValue.update( - "${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}", - ) - } - - val willSent = getString( - R.string.send_total_subtitle_format, - "${receipt.willSentCrypto} ${receipt.symbols.crypto}", - ) - llTotalContainer.tvWillBeSentValue.update(willSent) - } - ReceiptLayoutType.CRYPTO -> { - val receipt = state.crypto ?: return - - totalLayout.show(true) - totalTokenLayout.show(false) - tvReceiptAmountValue.update("${receipt.amountCrypto} ${receipt.symbols.crypto}") - tvReceiptFeeValue.update("${receipt.feeCrypto} ${receipt.symbols.crypto}") - llTotalContainer.tvTotalValue.post { - llTotalContainer.tvTotalValue.update("${receipt.totalCrypto} ${receipt.symbols.crypto}") - } - - if (receipt.willSentFiat == BigDecimalFormatter.EMPTY_BALANCE_SIGN) { - llTotalContainer.tvWillBeSentValue.hide() - } else { - llTotalContainer.tvWillBeSentValue.show() - llTotalContainer.tvWillBeSentValue.update( - getString( - R.string.send_total_subtitle_fiat_format, - "${receipt.willSentFiat} ${receipt.symbols.fiat}", - "${receipt.feeFiat} ${receipt.symbols.fiat}", - ), - ) - } - } - ReceiptLayoutType.TOKEN_FIAT -> { - val receipt = state.tokenFiat ?: return - - totalLayout.show(true) - totalTokenLayout.show(false) - tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}") - tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}") - llTotalContainer.tvTotalValue.post { - llTotalContainer.tvTotalValue.update( - "${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}", - ) - } - - val willSent = getString( - R.string.send_total_subtitle_asset_format, - "${receipt.symbols.token ?: ""} ${receipt.willSentToken}", - "${receipt.symbols.crypto} ${receipt.willSentFeeCoin}", - ) - llTotalContainer.tvWillBeSentValue.update(willSent) - } - ReceiptLayoutType.TOKEN_CRYPTO -> { - val receipt = state.tokenCrypto ?: return - - totalLayout.show(false) - totalTokenLayout.show(true) - - tvReceiptAmountValue.update("${receipt.amountToken} ${receipt.symbols.token}") - tvReceiptFeeValue.update("${receipt.feeCoin} ${receipt.symbols.crypto}") - - val willSent = SpannableStringBuilder() - .bold { - append(roughOrEmpty(receipt.totalFiat)).append(" ") - append(receipt.symbols.fiat) - } - llTotalContainer.tvTotalTokenCryptoValue.update(willSent.toString()) - } - ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN -> { - val receipt = state.customTokenCrypto ?: return - - totalLayout.show(false) - totalTokenLayout.show(true) - - tvReceiptAmountValue.update( - "${receipt.amountToken} ${receipt.symbols.token ?: receipt.symbols.crypto}", - ) - tvReceiptFeeValue.update("${receipt.feeCoin} ${receipt.symbols.fee}") - - val willSent = SpannableStringBuilder() - .bold { - append(roughOrEmpty(receipt.totalFiat)).append(" ") - append(receipt.symbols.fiat) - } - llTotalContainer.tvTotalTokenCryptoValue.update(willSent.toString()) - } - ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN_FIAT -> { - val receipt = state.customTokenFiat ?: return - - totalLayout.show(true) - totalTokenLayout.show(false) - tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}") - tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}") - llTotalContainer.tvTotalValue.post { - llTotalContainer.tvTotalValue.update( - "${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}", - ) - } - - val willSent = getString( - R.string.send_total_subtitle_asset_format, - "${receipt.symbols.token ?: receipt.symbols.crypto} ${receipt.willSentToken}", - "${receipt.symbols.fee} ${receipt.willSentFeeCoin}", - ) - llTotalContainer.tvWillBeSentValue.update(willSent) - } - - ReceiptLayoutType.SAME_CURRENCY -> { - val receipt = state.sameCurrencyCrypto ?: return - - totalLayout.show(true) - totalTokenLayout.show(false) - tvReceiptAmountValue.update( - "${receipt.amountCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}", - ) - tvReceiptFeeValue.update("${receipt.feeCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}") - llTotalContainer.tvTotalValue.post { - llTotalContainer.tvTotalValue.update( - "${receipt.totalCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}", - ) - } - - if (receipt.willSentFiat == BigDecimalFormatter.EMPTY_BALANCE_SIGN) { - llTotalContainer.tvWillBeSentValue.hide() - } else { - llTotalContainer.tvWillBeSentValue.show() - llTotalContainer.tvWillBeSentValue.update( - getString( - R.string.send_total_subtitle_fiat_format, - "${receipt.willSentFiat} ${receipt.symbols.fiat}", - "${receipt.feeFiat} ${receipt.symbols.fiat}", - ), - ) - } - } - ReceiptLayoutType.SAME_CURRENCY_FIAT -> { - val receipt = state.sameCurrencyFiat ?: return - - totalLayout.show(true) - totalTokenLayout.show(false) - tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}") - tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}") - llTotalContainer.tvTotalValue.post { - llTotalContainer.tvTotalValue.update( - "${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}", - ) - } - - val willSent = getString( - R.string.send_total_subtitle_format, - "${receipt.willSentCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}", - ) - llTotalContainer.tvWillBeSentValue.update(willSent) - } - ReceiptLayoutType.UNKNOWN, null -> {} - } - } - } - - private companion object { - const val ROUGH_SIGN = "≈" - } -} \ No newline at end of file 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 abb2a86125..cfb77e6180 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 @@ -1,8 +1,6 @@ package com.tangem.tap.features.wallet.redux.middlewares -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager -import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter @@ -17,11 +15,8 @@ import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.TapError import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE -import com.tangem.tap.features.send.redux.PrepareSendScreen -import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens import com.tangem.tap.proxy.redux.DaggerGraphState @@ -45,9 +40,6 @@ object TradeCryptoMiddleware { } } - private val isSendRedesignedEnabled: Boolean - get() = store.inject(getDependency = DaggerGraphState::sendFeatureToggles).isRedesignedSendEnabled - @Suppress("LongMethod", "CyclomaticComplexMethod") private fun handle(state: () -> AppState?, action: TradeCryptoAction) { if (DemoHelper.tryHandle(state, action)) return @@ -62,20 +54,8 @@ object TradeCryptoMiddleware { cryptoCurrencyId = action.cryptoCurrencyId, yield = action.yield, ) - is TradeCryptoAction.SendToken -> { - if (isSendRedesignedEnabled) { - handleNewSendToken(action = action) - } else { - handleSendToken(action = action) - } - } - is TradeCryptoAction.SendCoin -> { - if (isSendRedesignedEnabled) { - handleNewSendCoin(action = action) - } else { - handleSendCoin(action = action) - } - } + is TradeCryptoAction.SendToken -> handleNewSendToken(action = action) + is TradeCryptoAction.SendCoin -> handleNewSendCoin(action = action) } } @@ -177,135 +157,6 @@ object TradeCryptoMiddleware { } } - private fun handleSendToken(action: TradeCryptoAction.SendToken) { - val currency = action.tokenCurrency - val blockchain = Blockchain.fromId(currency.network.id.value) - - scope.launch { - val walletManager = store.inject(DaggerGraphState::walletManagersFacade) - .getOrCreateWalletManager( - userWalletId = action.userWallet.walletId, - blockchain = blockchain, - derivationPath = currency.network.derivationPath.value, - ) - - if (walletManager == null) { - val error = TapError.UnsupportedState(stateError = "WalletManager is null") - FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError)) - store.dispatchErrorNotification(error) - return@launch - } - - val sendableAmount = walletManager.wallet.amounts.values.firstOrNull { - val amountType = it.type - amountType is AmountType.Token && amountType.token.contractAddress == currency.contractAddress - } - - store.dispatchOnMain( - action = PrepareSendScreen( - walletManager = walletManager, - feePaidCurrency = walletManager.wallet.blockchain.feePaidCurrency(), - currency = currency, - coinAmount = walletManager.wallet.amounts[AmountType.Coin], - coinRate = action.coinFiatRate, - tokenAmount = sendableAmount, - tokenRate = action.tokenFiatRate, - feeCurrencyRate = action.feeCurrencyStatus?.value?.fiatRate, - feeCurrencyDecimals = action.feeCurrencyStatus?.currency?.decimals ?: 0, - ), - ) - - val txInfo = action.transactionInfo - if (txInfo != null) { - store.dispatchOnMain( - SendAction.SendSpecificTransaction( - sendAmount = txInfo.amount, - destinationAddress = txInfo.destinationAddress, - transactionId = txInfo.transactionId, - ), - ) - } - - val route = AppRoute.Send( - currency = currency, - userWalletId = action.userWallet.walletId, - ) - - store.dispatchNavigationAction { push(route) } - } - } - - private fun handleSendCoin(action: TradeCryptoAction.SendCoin) { - if (action.transactionInfo?.tag != null) { - // avoid open old send if memo exists - return - } - val cryptoStatus = action.coinStatus - val currency = cryptoStatus.currency - val blockchain = Blockchain.fromId(currency.network.id.value) - - scope.launch { - val walletManager = store.inject(DaggerGraphState::walletManagersFacade) - .getOrCreateWalletManager( - userWalletId = action.userWallet.walletId, - blockchain = blockchain, - derivationPath = currency.network.derivationPath.value, - ) - - if (walletManager == null) { - val error = TapError.UnsupportedState(stateError = "WalletManager is null") - FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError)) - store.dispatchErrorNotification(error) - return@launch - } - - val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type == AmountType.Coin } - when (currency) { - is CryptoCurrency.Coin -> { - val amountToSend = sendableAmounts.find { it.currencySymbol == currency.symbol } - - if (amountToSend == null) { - val error = TapError.UnsupportedState(stateError = "Amount to send is null") - FirebaseCrashlytics.getInstance() - .recordException(IllegalStateException(error.stateError)) - store.dispatchErrorNotification(error) - return@launch - } - - store.dispatchOnMain( - action = PrepareSendScreen( - walletManager = walletManager, - feePaidCurrency = walletManager.wallet.blockchain.feePaidCurrency(), - currency = currency, - coinAmount = amountToSend, - coinRate = cryptoStatus.value.fiatRate, - feeCurrencyRate = action.feeCurrencyStatus?.value?.fiatRate, - feeCurrencyDecimals = action.feeCurrencyStatus?.currency?.decimals ?: 0, - ), - ) - } - is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token") - } - - val txInfo = action.transactionInfo - if (txInfo != null) { - store.dispatchOnMain( - SendAction.SendSpecificTransaction( - sendAmount = txInfo.amount, - destinationAddress = txInfo.destinationAddress, - transactionId = txInfo.transactionId, - ), - ) - } - - val route = AppRoute.Send( - currency = currency, - userWalletId = action.userWallet.walletId, - ) - store.dispatchNavigationAction { push(route) } - } - } - private fun handleNewSendToken(action: TradeCryptoAction.SendToken) { handleNewSend( userWalletId = action.userWallet.walletId, 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 ee435bc6ec..45b2035717 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 @@ -32,7 +32,6 @@ import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter -import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter @@ -64,7 +63,6 @@ data class DaggerGraphState( val balanceHidingRepository: BalanceHidingRepository? = null, val walletsRepository: WalletsRepository? = null, val networksRepository: NetworksRepository? = null, - val sendFeatureToggles: SendFeatureToggles? = null, val sendRouter: SendRouter? = null, val qrScanningRouter: QrScanningRouter? = null, val currenciesRepository: CurrenciesRepository? = null, diff --git a/app/src/main/res/layout/fragment_send.xml b/app/src/main/res/layout/fragment_send.xml deleted file mode 100644 index a7f8c54c42..0000000000 --- a/app/src/main/res/layout/fragment_send.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/layout_send_address.xml b/app/src/main/res/layout/layout_send_address.xml deleted file mode 100644 index 1256f33037..0000000000 --- a/app/src/main/res/layout/layout_send_address.xml +++ /dev/null @@ -1,329 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/layout_send_amount.xml b/app/src/main/res/layout/layout_send_amount.xml deleted file mode 100644 index b8f987e234..0000000000 --- a/app/src/main/res/layout/layout_send_amount.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_send_fee.xml b/app/src/main/res/layout/layout_send_fee.xml deleted file mode 100644 index 66c3b128f1..0000000000 --- a/app/src/main/res/layout/layout_send_fee.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_send_receipt.xml b/app/src/main/res/layout/layout_send_receipt.xml deleted file mode 100644 index 97e7afbb0f..0000000000 --- a/app/src/main/res/layout/layout_send_receipt.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_snackbar_max_amount.xml b/app/src/main/res/layout/view_snackbar_max_amount.xml deleted file mode 100644 index 141e31aa66..0000000000 --- a/app/src/main/res/layout/view_snackbar_max_amount.xml +++ /dev/null @@ -1,5 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_snackbar_max_amount_content.xml b/app/src/main/res/layout/view_snackbar_max_amount_content.xml deleted file mode 100644 index de8cdfa27e..0000000000 --- a/app/src/main/res/layout/view_snackbar_max_amount_content.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - \ 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 bde5433abf..c3f822ed71 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": "REDESIGNED_SEND_SCREEN_ENABLED", - "version": "5.10.0" - }, { "name": "LOCAL_USER_LOGS_ENABLED", "version": "5.13.0" diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt deleted file mode 100644 index 453a79b60c..0000000000 --- a/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.send.api.featuretoggles - -/** - * Send feature toggles - */ -interface SendFeatureToggles { - - /** Availability of redesigned send screen */ - val isRedesignedSendEnabled: Boolean - - /** Updates remote toggle */ - suspend fun fetchNewSendEnabled() -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt deleted file mode 100644 index 60c8b2822c..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.features.send.impl.di - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.features.send.api.featuretoggles.SendFeatureToggles -import com.tangem.features.send.impl.featuretoggles.DefaultSendFeatureToggles -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 - -/** - * DI module provides implementation of [SendFeatureToggles] - */ -@Module -@InstallIn(SingletonComponent::class) -internal object SendFeatureTogglesModule { - - @Provides - @Singleton - fun provideSendFeatureToggles( - featureTogglesManager: FeatureTogglesManager, - tangemTechApi: TangemTechApi, - dispatchers: CoroutineDispatcherProvider, - ): SendFeatureToggles { - return DefaultSendFeatureToggles( - featureTogglesManager = featureTogglesManager, - tangemTechApi = tangemTechApi, - dispatchers = dispatchers, - ) - } -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt deleted file mode 100644 index b36cba4cbd..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.features.send.impl.featuretoggles - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.features.send.api.featuretoggles.SendFeatureToggles -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import timber.log.Timber - -/** - * Default implementation of Send feature toggles - * - * @property featureTogglesManager manager for getting information about the availability of feature toggles - * @property tangemTechApi api to get remote feature toggle for send - * @property dispatchers coroutine dispatchers - */ -internal class DefaultSendFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, -) : SendFeatureToggles { - - private val remoteSendEnabled: MutableStateFlow = MutableStateFlow(true) - - override val isRedesignedSendEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_SEND_SCREEN_ENABLED") && - remoteSendEnabled.value - - override suspend fun fetchNewSendEnabled() { - runCatching(dispatchers.io) { - tangemTechApi.getFeatures().getOrThrow() - }.onSuccess { response -> - remoteSendEnabled.update { response.isNewSendEnabled } - }.onFailure { - Timber.e(it.localizedMessage, "Unable to fetch new send toggle") - } - } -} \ No newline at end of file From 5f95aadd6ccd6a057398f4e69f84fcedf2df8e0b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 8 Aug 2024 12:28:07 +0300 Subject: [PATCH 38/87] Updated on 2026-08-14 --- .../src/main/res/values-zh-rTW/strings.xml | 2 +- .../main/res/drawable/img_staking_banner.xml | 345 ++++++++++++++++++ .../state/stub/StakingClickIntentsStub.kt | 2 + .../SetInitialDataStateTransformer.kt | 25 +- .../ShowInfoBottomSheetStateTransformer.kt | 4 +- .../ui/StakingInitialInfoContent.kt | 114 +++--- .../viewmodel/StakingClickIntents.kt | 2 + .../viewmodel/StakingViewModel.kt | 8 + 8 files changed, 417 insertions(+), 85 deletions(-) create mode 100644 core/ui/src/main/res/drawable/img_staking_banner.xml 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 b198f3eee7..acd0b259b8 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -48,7 +48,7 @@ 生物 購買 您尚未授予相機訪問權限,請更改您的隱私設置 - 刪除 + 删除 關閉 繼續 複製 diff --git a/core/ui/src/main/res/drawable/img_staking_banner.xml b/core/ui/src/main/res/drawable/img_staking_banner.xml new file mode 100644 index 0000000000..b3dfcacad4 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_staking_banner.xml @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index edda2d6732..3036764d3d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -18,6 +18,8 @@ object StakingClickIntentsStub : StakingClickIntents { override fun onPrevClick() {} + override fun onInitialInfoBannerClick() {} + override fun onInfoClick(infoType: InfoType) {} override fun onAmountValueChange(value: String) {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index a40933fac8..30711535be 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -87,8 +87,8 @@ internal class SetInitialDataStateTransformer( val yieldBalance = cryptoCurrencyStatus.value.yieldBalance return listOfNotNull( + createAnnualPercentageRateItem(), createAvailableItem(cryptoCurrencyStatus), - createApyItem(), createUnbondingPeriodItem(), createMinimumRequirementItem(cryptoCurrencyStatus), createRewardClaimingItem(), @@ -97,6 +97,15 @@ internal class SetInitialDataStateTransformer( ).toPersistentList() } + private fun createAnnualPercentageRateItem(): RoundedListWithDividersItemData { + return RoundedListWithDividersItemData( + id = R.string.staking_details_annual_percentage_rate, + startText = TextReference.Res(R.string.staking_details_annual_percentage_rate), + endText = getAprRange(), + iconClick = { clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE) }, + ) + } + private fun createAvailableItem(cryptoCurrencyStatus: CryptoCurrencyStatus): RoundedListWithDividersItemData { return RoundedListWithDividersItemData( id = R.string.staking_details_available, @@ -111,15 +120,6 @@ internal class SetInitialDataStateTransformer( ) } - private fun createApyItem(): RoundedListWithDividersItemData { - return RoundedListWithDividersItemData( - id = R.string.staking_details_annual_percentage_rate, - startText = TextReference.Res(R.string.staking_details_annual_percentage_rate), - endText = getAprRange(), - iconClick = { clickIntents.onInfoClick(InfoType.APY) }, - ) - } - private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData { return RoundedListWithDividersItemData( id = R.string.staking_details_unbonding_period, @@ -161,7 +161,10 @@ internal class SetInitialDataStateTransformer( ) } - private fun createWarmupPeriodItem(): RoundedListWithDividersItemData { + private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? { + val warmupPeriod = yield.metadata.warmupPeriod.days + if (warmupPeriod == 0) return null + return RoundedListWithDividersItemData( id = R.string.staking_details_warmup_period, startText = TextReference.Res(R.string.staking_details_warmup_period), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt index 755ca4552d..06f89bdd94 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt @@ -18,7 +18,7 @@ internal class ShowInfoBottomSheetStateTransformer( onDismissRequest = onDismiss, isShow = true, content = when (infoType) { - InfoType.APY -> StakingInfoBottomSheetConfig( + InfoType.ANNUAL_PERCENTAGE_RATE -> StakingInfoBottomSheetConfig( title = resourceReference(R.string.staking_details_annual_percentage_rate), text = resourceReference(R.string.staking_details_annual_percentage_rate_info), ) @@ -45,7 +45,7 @@ internal class ShowInfoBottomSheetStateTransformer( } enum class InfoType { - APY, + ANNUAL_PERCENTAGE_RATE, UNBONDING_PERIOD, REWARD_CLAIMING, WARMUP_PERIOD, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index c144d71513..51bb3380b1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -2,14 +2,13 @@ package com.tangem.features.staking.impl.presentation.ui import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.key @@ -17,8 +16,12 @@ 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.* 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.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -28,6 +31,7 @@ import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.components.list.roundedListWithDividersItems import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter @@ -45,9 +49,7 @@ import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.ImmutableList -// TODO staking metrics block is temporary disabled -// private const val METRICS_BLOCK_KEY = "MetricsBlock" - +private const val BANNER_BLOCK_KEY = "BannerBlock" private const val STAKING_REWARD_BLOCK_KEY = "StakingRewardBlock" private const val ACTIVE_STAKING_BLOCK_KEY = "ActiveStakingBlock" @@ -61,16 +63,16 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl .background(TangemTheme.colors.background.secondary) .padding(horizontal = TangemTheme.dimens.spacing16), ) { - // TODO staking metrics block is temporary disabled - // https://www.figma.com/design/Vs6SkVsFnUPsSCNwlnVf5U?node-id=12484-35755#876661319 - // if (state.yieldBalance == InnerYieldBalanceState.Empty) { - // item(key = METRICS_BLOCK_KEY) { - // Column(modifier = Modifier.animateItemPlacement()) { - // MetricsBlock(state) - // SpacerH12() - // } - // } - // } + if (state.yieldBalance == InnerYieldBalanceState.Empty) { + item(key = BANNER_BLOCK_KEY) { + Column( + modifier = Modifier.animateItemPlacement(), + ) { + BannerBlock(onClick = clickIntents::onInitialInfoBannerClick) + SpacerH12() + } + } + } this.roundedListWithDividersItems( rows = state.infoItems, @@ -102,66 +104,31 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl } } -@Suppress("UnusedPrivateMember") @Composable -private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) { - Column( - modifier = Modifier - .background( - color = TangemTheme.colors.background.primary, - shape = RoundedCornerShape(TangemTheme.dimens.radius12), - ) - .padding(TangemTheme.dimens.spacing12) - .fillMaxWidth(), +private fun BannerBlock(onClick: () -> Unit) { + Box( + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = onClick, + ), ) { - Text( - text = stringResource(id = R.string.staking_details_metrics_block_header), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, + Image( + modifier = Modifier.fillMaxSize(), + painter = painterResource(R.drawable.img_staking_banner), + contentDescription = null, ) - Spacer(modifier = Modifier.height(TangemTheme.dimens.size8)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(modifier = Modifier.weight(1F)) { - Text( - text = stringResource(id = R.string.staking_details_apr), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - Text( - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), - text = state.aprRange.resolveReference(), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.accent, - ) - } - Column(modifier = Modifier.weight(1F)) { - Row { - Text( - modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), - text = stringResource(id = R.string.staking_details_market_rating), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - Icon( - modifier = Modifier - .size(TangemTheme.dimens.size16) - .align(Alignment.CenterVertically), - painter = painterResource(id = R.drawable.ic_alert_24), - contentDescription = null, - tint = TangemTheme.colors.text.tertiary, - ) + Text( + modifier = Modifier + .align(Alignment.CenterStart) + .padding(TangemTheme.dimens.spacing16), + text = buildAnnotatedString { + withStyle(SpanStyle(Brush.linearGradient(textGradientColors))) { + append(stringResource(R.string.staking_details_banner_text)) } - Text( - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), - text = "1", // TODO staking - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.accent, - ) - } - } + }, + style = TangemTheme.typography.h2, + ) } } @@ -271,6 +238,11 @@ private fun ActiveStakingBlock(groups: ImmutableList, onCli } } +private val textGradientColors = listOf( + TangemColorPalette.White, + Color(0xff8fb4df), +) + // region preview @Preview(showBackground = true, widthDp = 360) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index b5cb61db29..3734e90f19 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -22,6 +22,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onPrevClick() + fun onInitialInfoBannerClick() + fun onInfoClick(infoType: InfoType) override fun onAmountNext() = onNextClick(actionType = null) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index cdb9cbd9ca..a5fd8c4d07 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -207,6 +207,10 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onPrevClick() } + override fun onInitialInfoBannerClick() { + innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL) + } + override fun onInfoClick(infoType: InfoType) { stateController.update( ShowInfoBottomSheetStateTransformer(infoType) { @@ -401,4 +405,8 @@ internal class StakingViewModel @Inject constructor( (value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState == InnerConfirmationStakingState.ASSENT } + + companion object { + const val WHAT_IS_STAKING_ARTICLE_URL = "TODO staking" + } } \ No newline at end of file From 700731a40870511a3fed83102c0203aa42e03ab2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 8 Aug 2024 18:28:24 +0300 Subject: [PATCH 39/87] Updated on 2026-08-14 --- .../datasource/api/common/config/ApiConfig.kt | 50 +++++++++++++++++++ .../api/common/config/ApiEnvironment.kt | 10 ++++ .../datasource/api/common/config/Express.kt | 35 +++++++++++++ .../api/common/config/TangemTech.kt | 34 +++++++++++++ .../config/managers/ApiConfigsManager.kt | 17 +++++++ .../managers/MutableApiConfigsManager.kt | 19 +++++++ .../utils/AppPreferencesStoreExt.kt | 10 +++- .../analytics/DefaultAnalyticsRepository.kt | 6 +-- .../feedback/DefaultFeedbackRepository.kt | 4 +- ...ultPolkadotAccountHealthCheckRepository.kt | 4 +- .../swap/DefaultSwapTransactionRepository.kt | 4 +- 11 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MutableApiConfigsManager.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt new file mode 100644 index 0000000000..7c5b88f27a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -0,0 +1,50 @@ +package com.tangem.datasource.api.common.config + +/** + * Api config + * + * @property currentEnvironment current api environment + * + * @see API configuration + * +[REDACTED_AUTHOR] + */ +sealed class ApiConfig(open val currentEnvironment: ApiEnvironment) { + + /** Available environments with base url */ + abstract val environments: Map + + /** Unique id */ + val id: ID = initializeId() + + enum class ID { + Express, + TangemTech, + } + + /** Copy method for sealed class [ApiConfig] */ + fun copySealed(currentEnvironment: ApiEnvironment): ApiConfig { + return when (this) { + is Express -> copy(currentEnvironment = currentEnvironment) + is TangemTech -> copy(currentEnvironment = currentEnvironment) + } + } + + private fun initializeId(): ID { + return when (this) { + is Express -> ID.Express + is TangemTech -> ID.TangemTech + } + } + + companion object { + internal const val DEBUG_BUILD_TYPE = "debug" + internal const val INTERNAL_BUILD_TYPE = "internal" + internal const val MOCKED_BUILD_TYPE = "mocked" + internal const val EXTERNAL_BUILD_TYPE = "external" + internal const val RELEASE_BUILD_TYPE = "release" + + /** All api configs */ + fun values() = listOf(Express(), TangemTech()) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt new file mode 100644 index 0000000000..00945bf772 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.common.config + +/** + * Api environment + * +[REDACTED_AUTHOR] + */ +enum class ApiEnvironment { + DEV, STAGE, PROD +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt new file mode 100644 index 0000000000..5ebf06c017 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -0,0 +1,35 @@ +package com.tangem.datasource.api.common.config + +import com.tangem.datasource.BuildConfig + +/** + * Express [ApiConfig] + * + * @property currentEnvironment current api environment + */ +internal data class Express( + override val currentEnvironment: ApiEnvironment = initializeCurrentEnvironment(), +) : ApiConfig(currentEnvironment) { + + override val environments: Map = mapOf( + ApiEnvironment.DEV to "[REDACTED_ENV_URL]", + ApiEnvironment.STAGE to "[REDACTED_ENV_URL]", + ApiEnvironment.PROD to "https://express.tangem.com/v1/", + ) + + private companion object { + + fun initializeCurrentEnvironment(): ApiEnvironment { + return when (BuildConfig.BUILD_TYPE) { + DEBUG_BUILD_TYPE -> ApiEnvironment.DEV + INTERNAL_BUILD_TYPE, + MOCKED_BUILD_TYPE, + -> ApiEnvironment.STAGE + EXTERNAL_BUILD_TYPE, + RELEASE_BUILD_TYPE, + -> ApiEnvironment.PROD + else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") + } + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt new file mode 100644 index 0000000000..25d15d3194 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt @@ -0,0 +1,34 @@ +package com.tangem.datasource.api.common.config + +import com.tangem.datasource.BuildConfig + +/** + * TangemTech [ApiConfig] + * + * @property currentEnvironment current api environment + */ +internal data class TangemTech( + override val currentEnvironment: ApiEnvironment = initializeCurrentEnvironment(), +) : ApiConfig(currentEnvironment) { + + override val environments: Map = mapOf( + ApiEnvironment.DEV to "https://devapi.tangem-tech.com/v1/", + ApiEnvironment.PROD to "https://api.tangem-tech.com/v1/", + ) + + private companion object { + + fun initializeCurrentEnvironment(): ApiEnvironment { + return when (BuildConfig.BUILD_TYPE) { + DEBUG_BUILD_TYPE, + INTERNAL_BUILD_TYPE, + -> ApiEnvironment.DEV + MOCKED_BUILD_TYPE, + EXTERNAL_BUILD_TYPE, + RELEASE_BUILD_TYPE, + -> ApiEnvironment.PROD + else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") + } + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt new file mode 100644 index 0000000000..6bb9cbb6ca --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.api.common.config.managers + +import com.tangem.datasource.api.common.config.ApiConfig + +/** + * Api configs manager + * +[REDACTED_AUTHOR] + */ +interface ApiConfigsManager { + + /** Initialize resources */ + suspend fun initialize() {} + + /** Get base url of api by [id] */ + fun getBaseUrl(id: ApiConfig.ID): String +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MutableApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MutableApiConfigsManager.kt new file mode 100644 index 0000000000..87ec3f21fb --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MutableApiConfigsManager.kt @@ -0,0 +1,19 @@ +package com.tangem.datasource.api.common.config.managers + +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import kotlinx.coroutines.flow.Flow + +/** + * Mutable [ApiConfigsManager] for change information about the current api environment + * +[REDACTED_AUTHOR] + */ +interface MutableApiConfigsManager : ApiConfigsManager { + + /** Configs */ + val configs: Flow> + + /** Change api environment [environment] by [id] */ + suspend fun changeEnvironment(id: String, environment: ApiEnvironment) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt index 26be12c002..a7fd3cafef 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -125,7 +125,7 @@ suspend inline fun AppPreferencesStore.storeObjectMap( } /** Get map with [String] key and value [V] by string [key], or empty if data is not found */ -suspend inline fun AppPreferencesStore.getObjectMap(key: Preferences.Key): Map { +suspend inline fun AppPreferencesStore.getObjectMapSync(key: Preferences.Key): Map { val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) val adapter = moshi.adapter>(type) @@ -135,6 +135,14 @@ suspend inline fun AppPreferencesStore.getObjectMap(key: Preferences .orEmpty() } +/** Get flow of map with [String] key and value [V] by string [key], or empty if data is not found */ +inline fun AppPreferencesStore.getObjectMap(key: Preferences.Key): Flow> { + val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) + val adapter = moshi.adapter>(type) + + return data.map { it[key]?.let(adapter::fromJson) ?: emptyMap() } +} + /** Get set of data [T] by string [key], or empty if data is not found */ suspend inline fun AppPreferencesStore.getObjectSetSync(key: Preferences.Key): Set { val adapter = moshi.adapter>(Types.newParameterizedType(Set::class.java, T::class.java)) diff --git a/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt b/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt index 4866aa1c89..b1554751cc 100644 --- a/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt +++ b/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt @@ -3,7 +3,7 @@ package com.tangem.data.analytics import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectListSync -import com.tangem.datasource.local.preferences.utils.getObjectMap +import com.tangem.datasource.local.preferences.utils.getObjectMapSync import com.tangem.domain.analytics.model.WalletBalanceState import com.tangem.domain.analytics.repository.AnalyticsRepository import com.tangem.domain.wallets.models.UserWalletId @@ -29,7 +29,7 @@ internal class DefaultAnalyticsRepository( } override suspend fun getWalletBalanceState(userWalletId: UserWalletId): WalletBalanceState? { - val walletsBalanceState = appPreferencesStore.getObjectMap( + val walletsBalanceState = appPreferencesStore.getObjectMapSync( key = PreferencesKeys.WALLETS_BALANCES_STATES_KEY, ) @@ -41,7 +41,7 @@ internal class DefaultAnalyticsRepository( val walletsBalanceState = it.getObjectMap( key = PreferencesKeys.WALLETS_BALANCES_STATES_KEY, ) - val updatedWalletsBalanceState = walletsBalanceState.orEmpty() + val updatedWalletsBalanceState = walletsBalanceState .plus(pair = userWalletId.stringValue to balanceState) it.setObjectMap(PreferencesKeys.WALLETS_BALANCES_STATES_KEY, updatedWalletsBalanceState) 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 b5313ac608..26278b5877 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 @@ -8,7 +8,7 @@ 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.preferences.utils.getObjectMapSync import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.repository.FeedbackRepository @@ -99,7 +99,7 @@ internal class DefaultFeedbackRepository( } override suspend fun getAppLogs(): List { - return appPreferencesStore.getObjectMap(key = PreferencesKeys.APP_LOGS_KEY) + return appPreferencesStore.getObjectMapSync(key = PreferencesKeys.APP_LOGS_KEY) .map { AppLogModel(timestamp = it.key.toLong(), message = it.value) } .sortedBy(AppLogModel::timestamp) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultPolkadotAccountHealthCheckRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultPolkadotAccountHealthCheckRepository.kt index 049991280b..82f9f9465a 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultPolkadotAccountHealthCheckRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultPolkadotAccountHealthCheckRepository.kt @@ -9,7 +9,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY -import com.tangem.datasource.local.preferences.utils.getObjectMap +import com.tangem.datasource.local.preferences.utils.getObjectMapSync import com.tangem.datasource.local.preferences.utils.getObjectSetSync import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository @@ -97,7 +97,7 @@ internal class DefaultPolkadotAccountHealthCheckRepository( runCatching { do { // Getting batch of extrinsics to check - val lastChecked = appPreferencesStore.getObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY) + val lastChecked = appPreferencesStore.getObjectMapSync(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY) val lastExtrinsic = lastChecked[address] val extrinsicListResult = accountCheckerProvider.getExtrinsicList(afterExtrinsicId = lastExtrinsic) extrinsicListResult.extrinsic diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index 7cca09753d..9b44aacbef 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -4,7 +4,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.datasource.local.preferences.utils.getObjectListSync -import com.tangem.datasource.local.preferences.utils.getObjectMap +import com.tangem.datasource.local.preferences.utils.getObjectMapSync import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId @@ -75,7 +75,7 @@ class DefaultSwapTransactionRepository( scanResponse: ScanResponse, ): Flow?> { return withContext(dispatchers.io) { - val txStatuses = appPreferencesStore.getObjectMap( + val txStatuses = appPreferencesStore.getObjectMapSync( key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, ) appPreferencesStore.getObjectList( From c500f4530770b605e4de2768ab720a7943ec24fa Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 8 Aug 2024 18:53:25 +0300 Subject: [PATCH 40/87] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 7 + .../tangem/common/ui/charts/MarketChart.kt | 16 +- .../ui/charts/downsample/LTThreeBuckets.kt | 2 +- .../api/markets/TangemTechMarketsApi.kt | 1 + .../response/TokenMarketInfoResponse.kt | 38 ++--- .../bottomsheets/TangemBottomSheet.kt | 11 +- .../tangem/core/ui/res/TangemAnimations.kt | 11 +- .../core/ui/utils/BigDecimalFormatter.kt | 94 ++++++++---- .../ui/utils/BigDecimalFormatterCompat.kt | 27 +++- .../com/tangem/core/ui/utils/ScrollUtils.kt | 15 ++ .../com/tangem/utils/SupportedLanguages.kt | 35 +++++ .../markets/DefaultMarketsTokenRepository.kt | 7 +- .../converters/TokenMarketInfoConverter.kt | 16 +- domain/markets/build.gradle.kts | 1 + .../tangem/domain/markets/TokenMarketInfo.kt | 32 ++-- .../markets/GetTokenMarketInfoUseCase.kt | 2 + .../repositories/MarketsTokenRepository.kt | 2 +- features/markets/impl/build.gradle.kts | 1 + .../impl/model/MarketsTokenDetailsModel.kt | 136 ++++++++++++++++- .../model/converters/DescriptionConverter.kt | 41 +++++ .../model/converters/InsightsConverter.kt | 131 ++++++++++++++++ .../impl/model/converters/LinksConverter.kt | 49 ++++++ .../impl/model/converters/MetricsConverter.kt | 135 +++++++++++++++++ .../converters/PricePerformanceConverter.kt | 58 +++++++ .../converters/SecurityScoreConverter.kt | 35 +++++ .../converters/TokenMarketInfoConverter.kt | 34 +++++ .../impl/ui/MarketsTokenDetailsContent.kt | 85 +++++++---- .../details/impl/ui/components/Description.kt | 112 ++++++++++++++ .../impl/ui/components/InfoBottomSheet.kt | 47 ++++++ .../details/impl/ui/components/InfoPoint.kt | 21 +-- .../impl/ui/components/InsightsBlock.kt | 4 +- .../details/impl/ui/components/LinksBlock.kt | 20 +-- .../ui/components/MarketTokenDetailsChart.kt | 7 +- .../impl/ui/components/MetricsBlock.kt | 4 +- .../ui/components/PricePerformanceBlock.kt | 36 ++--- .../impl/ui/components/SecurityScoreBlock.kt | 2 +- .../ui/components/TokenMarketDetailsBody.kt | 142 ++++++++++++++++++ .../details/impl/ui/entity/InsightsUM.kt | 11 -- .../markets/details/impl/ui/entity/LinksUM.kt | 22 --- .../impl/ui/state/InfoBottomSheetContent.kt | 9 ++ .../impl/ui/{entity => state}/InfoPointUM.kt | 4 +- .../details/impl/ui/state/InsightsUM.kt | 9 ++ .../markets/details/impl/ui/state/LinksUM.kt | 19 +++ .../MarketsTokenDetailsUM.kt | 35 ++++- .../impl/ui/{entity => state}/MetricsUM.kt | 4 +- .../{entity => state}/PricePerformanceUM.kt | 5 +- .../ui/{entity => state}/SecurityScoreUM.kt | 4 +- .../tokenlist/impl/model/MarketsListModel.kt | 13 +- .../converters/MarketsTokenItemConverter.kt | 9 +- .../MarketsListBatchFlowManager.kt | 6 +- .../statemanager/MarketsListUMStateManager.kt | 10 +- .../markets/tokenlist/impl/ui/MarketsList.kt | 8 +- .../impl/ui/components/MarketsListItem.kt | 2 +- .../ui/components/MarketsListLazyColumn.kt | 17 +-- .../MarketsListSortByBottomSheet.kt | 4 +- .../MarketChartListItemPreviewDataProvider.kt | 2 +- .../ui/{entity => state}/MarketsListItemUM.kt | 2 +- .../ui/{entity => state}/MarketsListUM.kt | 2 +- .../SortByBottomSheetContentUM.kt | 2 +- 59 files changed, 1338 insertions(+), 278 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/ScrollUtils.kt create mode 100644 core/utils/src/main/java/com/tangem/utils/SupportedLanguages.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InsightsUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/LinksUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/{entity => state}/InfoPointUM.kt (62%) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/{entity => state}/MarketsTokenDetailsUM.kt (64%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/{entity => state}/MetricsUM.kt (54%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/{entity => state}/PricePerformanceUM.kt (70%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/{entity => state}/SecurityScoreUM.kt (63%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/{entity => state}/MarketsListItemUM.kt (94%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/{entity => state}/MarketsListUM.kt (97%) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/{entity => state}/SortByBottomSheetContentUM.kt (79%) diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index f07561bf15..e96df425a1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.GetTokenMarketInfoUseCase import com.tangem.domain.markets.GetTokenPriceChartUseCase import com.tangem.domain.markets.repositories.MarketsTokenRepository import dagger.Module @@ -26,4 +27,10 @@ object MarketsDomainModule { fun provideGetTokenPriceChartUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenPriceChartUseCase { return GetTokenPriceChartUseCase(marketsTokenRepository = marketsTokenRepository) } + + @Provides + @Singleton + fun provideGetTokenMarketInfoUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenMarketInfoUseCase { + return GetTokenMarketInfoUseCase(marketsTokenRepository = marketsTokenRepository) + } } \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt index 4e43d74718..917b2eab45 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -10,6 +10,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity @@ -20,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.resolveAsTypeface import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent @@ -71,7 +73,6 @@ private const val GUIDELINES_COUNT = 3 * @param splitChartSegmentColor The color of the grayed by marker chart segment. * @param backgroundSplitChartSegmentColorAlpha The alpha of the background the [splitChartSegmentColor] * @param backgroundColorAlpha The alpha of the background color of the chart. - * @param noChartContent A composable function that defines the content to be displayed when there is no data to display. */ @Composable fun MarketChart( @@ -108,7 +109,7 @@ fun MarketChart( // we need to calculate what the overall height should be in order to get the correct height of the graph val bottomAxisHeight = with(LocalDensity.current) { - TangemTheme.typography.caption2.fontSize.toPx().toInt() + TangemTheme.dimens.spacing26.toPx().toInt() + getMarketChartBottomAxisHeight().toPx().toInt() } CartesianChartHost( @@ -120,6 +121,10 @@ fun MarketChart( } else { 0 } + } + // Sometimes the chart is not drawn correctly (ex. in LazyLayout), so we need to force the redraw + .drawBehind { + state.markerFraction }, chart = chart, modelProducer = state.modelProducer, @@ -129,6 +134,13 @@ fun MarketChart( ) } +@Composable +fun getMarketChartBottomAxisHeight(): Dp { + return with(LocalDensity.current) { + TangemTheme.typography.caption2.fontSize.toDp() + TangemTheme.dimens.spacing26 + } +} + @Composable private fun rememberMarketVisibilityListener( canvasWidth: Int, diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/downsample/LTThreeBuckets.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/downsample/LTThreeBuckets.kt index 59bf621547..21578b9cc1 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/downsample/LTThreeBuckets.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/downsample/LTThreeBuckets.kt @@ -238,7 +238,7 @@ private data class Area( } } -inline fun List.fastForEach(action: (T) -> Unit) { +private inline fun List.fastForEach(action: (T) -> Unit) { for (index in indices) { val item = get(index) action(item) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt index 7362229f4a..c1cc2d5702 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -24,6 +24,7 @@ interface TangemTechMarketsApi { suspend fun getCoinMarketData( @Path("coin_id") coinId: String, @Query("currency") currency: String, + @Query("language") language: String, ): ApiResponse @GET("coins/{coin_id}/history") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index 13b5e0bdee..ef485e1829 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -21,7 +21,7 @@ data class TokenMarketInfoResponse( @Json(name = "full_description") val fullDescription: String?, @Json(name = "insights") - val insights: List?, + val insights: Insights?, @Json(name = "metrics") val metrics: Metrics?, @Json(name = "links") @@ -32,33 +32,33 @@ data class TokenMarketInfoResponse( data class PriceChangePercentage( @Json(name = "24h") - val day: Int?, + val day: BigDecimal?, @Json(name = "1w") - val week: Int?, + val week: BigDecimal?, @Json(name = "1m") - val month: Int?, + val month: BigDecimal?, @Json(name = "3m") - val threeMonths: Int?, + val threeMonths: BigDecimal?, @Json(name = "6m") - val sixMonths: Int?, + val sixMonths: BigDecimal?, @Json(name = "1y") - val year: Int?, + val year: BigDecimal?, @Json(name = "all_time") - val allTime: Int?, + val allTime: BigDecimal?, ) data class Network( @Json(name = "network_id") val networkId: String, @Json(name = "exchangeable") - val exchangeable: Boolean, + val exchangeable: Boolean = false, @Json(name = "contract_address") - val contractAddress: String, + val contractAddress: String?, @Json(name = "decimal_count") - val decimalCount: Int, + val decimalCount: Int?, ) - data class Insight( + data class Insights( @Json(name = "holders_change") val holdersChange: Change?, @Json(name = "liquidity_change") @@ -71,11 +71,11 @@ data class TokenMarketInfoResponse( data class Change( @Json(name = "24h") - val day: Int?, + val day: BigDecimal?, @Json(name = "1w") - val week: Int?, + val week: BigDecimal?, @Json(name = "1m") - val month: Int?, + val month: BigDecimal?, ) data class Metrics( @@ -123,9 +123,9 @@ data class TokenMarketInfoResponse( ) data class Range( - @Json(name = "low") - val low: Int?, - @Json(name = "high") - val high: Int?, + @Json(name = "low_price") + val low: BigDecimal?, + @Json(name = "high_price") + val high: BigDecimal?, ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 388e6b69c2..6ef3e53d1f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -30,6 +30,7 @@ inline fun TangemBottomSheet( titleAction: TopAppBarButtonUM? = null, containerColor: Color = TangemTheme.colors.background.primary, addBottomInsets: Boolean = true, + skipPartiallyExpanded: Boolean = true, crossinline content: @Composable ColumnScope.(T) -> Unit, ) { TangemBottomSheet( @@ -37,6 +38,7 @@ inline fun TangemBottomSheet( containerColor = containerColor, addBottomInsets = addBottomInsets, title = { TangemBottomSheetTitle(title = titleText, endButton = titleAction) }, + skipPartiallyExpanded = skipPartiallyExpanded, content = content, ) } @@ -49,6 +51,7 @@ inline fun TangemBottomSheet( config: TangemBottomSheetConfig, containerColor: Color = TangemTheme.colors.background.primary, addBottomInsets: Boolean = true, + skipPartiallyExpanded: Boolean = true, crossinline title: @Composable BoxScope.(T) -> Unit = {}, crossinline content: @Composable ColumnScope.(T) -> Unit, ) { @@ -61,6 +64,7 @@ inline fun TangemBottomSheet( addBottomInsets = addBottomInsets, title = title, content = content, + skipPartiallyExpanded = skipPartiallyExpanded, ) } else { DefaultBottomSheet( @@ -69,6 +73,7 @@ inline fun TangemBottomSheet( addBottomInsets = addBottomInsets, title = title, content = content, + skipPartiallyExpanded = skipPartiallyExpanded, ) } } @@ -79,11 +84,12 @@ inline fun DefaultBottomSheet( config: TangemBottomSheetConfig, containerColor: Color, addBottomInsets: Boolean, + skipPartiallyExpanded: Boolean = true, crossinline title: @Composable (BoxScope.(T) -> Unit), crossinline content: @Composable (ColumnScope.(T) -> Unit), ) { var isVisible by remember { mutableStateOf(value = config.isShow) } - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded) if (isVisible && config.content is T) { BasicBottomSheet( @@ -111,6 +117,7 @@ inline fun PreviewBottomSheet( config: TangemBottomSheetConfig, containerColor: Color, addBottomInsets: Boolean, + skipPartiallyExpanded: Boolean = true, crossinline title: @Composable (BoxScope.(T) -> Unit), crossinline content: @Composable (ColumnScope.(T) -> Unit), ) { @@ -118,7 +125,7 @@ inline fun PreviewBottomSheet( modifier = Modifier.width(360.dp), config = config, sheetState = SheetState( - skipPartiallyExpanded = true, + skipPartiallyExpanded = skipPartiallyExpanded, initialValue = Expanded, density = LocalDensity.current, ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt index b735f815a7..2e81db1003 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt @@ -1,10 +1,7 @@ package com.tangem.core.ui.res import androidx.compose.animation.* -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.* import androidx.compose.runtime.* @Immutable @@ -24,10 +21,6 @@ object TangemAnimations { @Immutable object TransitionSpecs { - - @Stable - val textChange: ContentTransform = - fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)) togetherWith - fadeOut(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)) + // TODO add more transition specs } } \ No newline at end of file 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 e5d94f85b7..40e51a085c 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 @@ -117,6 +117,7 @@ object BigDecimalFormatter { fiatAmount: BigDecimal?, fiatCurrencyCode: String, fiatCurrencySymbol: String, + decimals: Int = FIAT_MARKET_DEFAULT_DIGITS, locale: Locale = Locale.getDefault(), ): String { if (fiatAmount == null) return EMPTY_BALANCE_SIGN @@ -124,8 +125,8 @@ object BigDecimalFormatter { val formatterCurrency = getCurrency(fiatCurrencyCode) val formatter = NumberFormat.getCurrencyInstance(locale).apply { currency = formatterCurrency - maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS - minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + maximumFractionDigits = decimals + minimumFractionDigits = decimals roundingMode = RoundingMode.HALF_UP } @@ -250,40 +251,27 @@ object BigDecimalFormatter { /** * "123456.6" -> "$123.457K" * "12345.6" -> "$123.046K" + * Negative amount is not supported + * @param threeDigitsMethod if true, will format the amount always with 3 significant digits + * @param scale the number of digits to the right of the decimal point */ @Suppress("MagicNumber") - fun formatCompactAmount( - amount: BigDecimal, + fun formatCompactFiatAmount( + amount: BigDecimal?, fiatCurrencyCode: String, fiatCurrencySymbol: String, + threeDigitsMethod: Boolean = false, + scale: Int = 0, locale: Locale = Locale.getDefault(), ): String { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - return BigDecimalFormatterCompat.formatCompactAmountNoLocaleContext( - amount = amount, - fiatCurrencyCode = fiatCurrencyCode, - fiatCurrencySymbol = fiatCurrencySymbol, - locale = locale, - ) - } + if (amount == null) return EMPTY_BALANCE_SIGN - val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP) - val digitsCount = scaledAmount.longValueExact().toString().count() - val digitsToFormat = 6 - when (digitsCount % 3) { - 0 -> 0 - 1 -> 2 - else -> 1 - } - - val formatter = CompactDecimalFormat.getInstance( - locale, - CompactDecimalFormat.CompactStyle.SHORT, - ).apply { - minimumSignificantDigits = 4 - maximumSignificantDigits = digitsToFormat - } - - val rawAmount = formatter.format(amount.setScale(0, RoundingMode.HALF_UP)) + val rawAmount = formatCompactAmount( + amount = amount, + locale = locale, + threeDigitsMethod = threeDigitsMethod, + scale = scale, + ) return addCurrencySymbolToStringAmount( amount = rawAmount, @@ -293,5 +281,53 @@ object BigDecimalFormatter { ) } + /** + * "123456.6" -> "123.457K" + * "12345.6" -> "123.046K" + * Negative amount is not supported + * @param threeDigitsMethod if true, will format the amount always with 3 significant digits + * @param scale the number of digits to the right of the decimal point + */ + @Suppress("MagicNumber") + fun formatCompactAmount( + amount: BigDecimal, + locale: Locale = Locale.getDefault(), + threeDigitsMethod: Boolean = false, + scale: Int = 0, + ): String { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return BigDecimalFormatterCompat.formatCompactAmountNoLocaleContext(amount = amount) + } + + if (threeDigitsMethod) { + val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP) + val digitsCount = scaledAmount.longValueExact().toString().count() + val digitsToFormat = 6 - when (digitsCount % 3) { + 0 -> 0 + 1 -> 2 + else -> 1 + } + + val formatter = CompactDecimalFormat.getInstance( + locale, + CompactDecimalFormat.CompactStyle.SHORT, + ).apply { + minimumSignificantDigits = 4 + maximumSignificantDigits = digitsToFormat + } + + return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP)) + } else { + val value = amount.setScale(scale, RoundingMode.HALF_UP) + + val formatter = CompactDecimalFormat.getInstance( + locale, + CompactDecimalFormat.CompactStyle.SHORT, + ) + + return formatter.format(value) + } + } + private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatterCompat.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatterCompat.kt index 82aab46559..9218188bef 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatterCompat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatterCompat.kt @@ -7,16 +7,32 @@ import java.util.Locale internal object BigDecimalFormatterCompat { /** - * Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes + * Formats value as [BigDecimalFormatter.formatCompactFiatAmount] does using only "T","B","M","K" suffixes * Used for < API24 compatibility */ @Suppress("MagicNumber", "UnnecessaryParentheses") - fun formatCompactAmountNoLocaleContext( + fun formatCompactFiatAmountNoLocaleContext( amount: BigDecimal, fiatCurrencyCode: String, fiatCurrencySymbol: String, locale: Locale = Locale.getDefault(), ): String { + val formatted = formatCompactAmountNoLocaleContext(amount) + + return BigDecimalFormatter.addCurrencySymbolToStringAmount( + amount = formatted, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + locale = locale, + ) + } + + /** + * Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes + * Used for < API24 compatibility + */ + @Suppress("MagicNumber", "UnnecessaryParentheses") + fun formatCompactAmountNoLocaleContext(amount: BigDecimal): String { val value = amount.setScale(0, RoundingMode.HALF_UP).longValueExact() val formatted = when { @@ -42,11 +58,6 @@ internal object BigDecimalFormatterCompat { else -> return value.toString() } - return BigDecimalFormatter.addCurrencySymbolToStringAmount( - amount = formatted, - fiatCurrencyCode = fiatCurrencyCode, - fiatCurrencySymbol = fiatCurrencySymbol, - locale = locale, - ) + return formatted } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/ScrollUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/ScrollUtils.kt new file mode 100644 index 0000000000..3cef274d3a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/ScrollUtils.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.utils + +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll + +fun Modifier.disableNestedScroll(): Modifier = nestedScroll(DisableParentConnection) + +private object DisableParentConnection : NestedScrollConnection { + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + return available.copy(x = 0f) + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/SupportedLanguages.kt b/core/utils/src/main/java/com/tangem/utils/SupportedLanguages.kt new file mode 100644 index 0000000000..48b2788b37 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/SupportedLanguages.kt @@ -0,0 +1,35 @@ +package com.tangem.utils + +import java.util.Locale + +object SupportedLanguages { + const val ENGLISH = "en" + const val RUSSIAN = "ru" + const val GERMAN = "de" + const val FRANCH = "fr" + const val ITALIAN = "it" + const val JAPANESE = "ja" + const val UKRAINIAN = "uk" + const val CHINESE = "uk" + + val supportedLangugeCodes = listOf( + ENGLISH, + RUSSIAN, + GERMAN, + FRANCH, + ITALIAN, + JAPANESE, + UKRAINIAN, + CHINESE, + ) + + fun getCurrentSupportedLanguageCode(): String { + val locale = Locale.getDefault() + + return if (supportedLangugeCodes.contains(locale.language)) { + locale.language + } else { + ENGLISH + } + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 6bd59980f7..e670fce633 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -108,10 +108,15 @@ internal class DefaultMarketsTokenRepository( return tokenChartConverter.convert(interval, response.getOrThrow()) } - override suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String): TokenMarketInfo { + override suspend fun getTokenInfo( + fiatCurrencyCode: String, + tokenId: String, + languageCode: String, + ): TokenMarketInfo { val response = marketsApi.getCoinMarketData( currency = fiatCurrencyCode, coinId = tokenId, + language = languageCode, ) return TokenMarketInfoConverter().convert(response.getOrThrow()) diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt index 524d27debd..8693393e68 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -50,15 +50,13 @@ internal class TokenMarketInfoConverter : Converter.convert(): List { - return map { - TokenMarketInfo.Insight( - holdersChange = it.holdersChange?.convert(), - liquidityChange = it.liquidityChange?.convert(), - buyPressureChange = it.buyPressureChange?.convert(), - experiencedBuyerChange = it.experiencedBuyerChange?.convert(), - ) - } + private fun TokenMarketInfoResponse.Insights.convert(): TokenMarketInfo.Insights { + return TokenMarketInfo.Insights( + holdersChange = holdersChange?.convert(), + liquidityChange = liquidityChange?.convert(), + buyPressureChange = buyPressureChange?.convert(), + experiencedBuyerChange = experiencedBuyerChange?.convert(), + ) } private fun TokenMarketInfoResponse.Change.convert(): TokenMarketInfo.Change { diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index 8545d1c3b3..885998c263 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -18,4 +18,5 @@ dependencies { implementation(deps.kotlin.serialization) implementation(projects.domain.tokens.models) + implementation(projects.core.utils) } \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt index 1ad9daf777..e1cf490632 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt @@ -11,29 +11,29 @@ data class TokenMarketInfo( val networks: List?, val shortDescription: String?, val fullDescription: String?, - val insights: List?, + val insights: Insights?, val metrics: Metrics?, val links: Links?, val pricePerformance: PricePerformance?, ) { data class PriceChangePercentage( - val day: Int?, - val week: Int?, - val month: Int?, - val threeMonths: Int?, - val sixMonths: Int?, - val year: Int?, - val allTime: Int?, + val day: BigDecimal?, + val week: BigDecimal?, + val month: BigDecimal?, + val threeMonths: BigDecimal?, + val sixMonths: BigDecimal?, + val year: BigDecimal?, + val allTime: BigDecimal?, ) data class Network( val networkId: String, val exchangeable: Boolean, - val contractAddress: String, - val decimalCount: Int, + val contractAddress: String?, + val decimalCount: Int?, ) - data class Insight( + data class Insights( val holdersChange: Change?, val liquidityChange: Change?, val buyPressureChange: Change?, @@ -41,9 +41,9 @@ data class TokenMarketInfo( ) data class Change( - val day: Int?, - val week: Int?, - val month: Int?, + val day: BigDecimal?, + val week: BigDecimal?, + val month: BigDecimal?, ) data class Metrics( @@ -75,7 +75,7 @@ data class TokenMarketInfo( ) data class Range( - val low: Int?, - val high: Int?, + val low: BigDecimal?, + val high: BigDecimal?, ) } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt index f6b2ecf317..c7935cb4e0 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.markets import arrow.core.Either import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.utils.SupportedLanguages class GetTokenMarketInfoUseCase( private val marketsTokenRepository: MarketsTokenRepository, @@ -13,6 +14,7 @@ class GetTokenMarketInfoUseCase( marketsTokenRepository.getTokenInfo( fiatCurrencyCode = appCurrency.code, tokenId = tokenId, + languageCode = SupportedLanguages.getCurrentSupportedLanguageCode(), ) }.mapLeft {} } diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index 0fd36a20fa..e6e3a23e03 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -12,5 +12,5 @@ interface MarketsTokenRepository { suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart - suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String): TokenMarketInfo + suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String, languageCode: String): TokenMarketInfo } \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index f2becec495..7d8b7d0667 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { /* Project - API */ api(projects.features.markets.api) + implementation(projects.core.navigation) /* Domain */ implementation(projects.domain.markets) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index f1814f03df..2359614915 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -5,6 +5,9 @@ import arrow.core.getOrElse import com.tangem.common.ui.charts.state.* import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList @@ -13,11 +16,16 @@ import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.GetTokenMarketInfoUseCase import com.tangem.domain.markets.GetTokenPriceChartUseCase import com.tangem.domain.markets.PriceChangeInterval import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent -import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM +import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter +import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter +import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent +import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.impl.R +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -28,12 +36,15 @@ import java.math.BigDecimal import java.math.RoundingMode import javax.inject.Inject +@Suppress("LargeClass") @Stable internal class MarketsTokenDetailsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, + private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, + private val urlOpener: UrlOpener, ) : Model() { val params = paramsContainer.require() @@ -47,6 +58,21 @@ internal class MarketsTokenDetailsModel @Inject constructor( initialValue = params.appCurrency, ) + private val infoConverter = TokenMarketInfoConverter( + appCurrency = Provider { currentAppCurrency.value }, + onInfoClick = { + showInfoBottomSheet(it) + }, + onLinkClick = { + urlOpener.openUrl(it.url) + }, + ) + private val descriptionConverter = DescriptionConverter( + onReadModeClicked = { + showInfoBottomSheet(it) + }, + ) + private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) { chartData = MarketChartData.NoData.Loading @@ -89,19 +115,39 @@ internal class MarketsTokenDetailsModel @Inject constructor( chartState = MarketsTokenDetailsUM.ChartState( dataProducer = chartDataProducer, chartLook = MarketChartLook(), - onLoadRetryClick = {}, + onLoadRetryClick = ::onLoadRetryClicked, status = MarketsTokenDetailsUM.ChartState.Status.LOADING, onMarkerPointSelected = ::onMarkerPointSelected, ), selectedInterval = PriceChangeInterval.H24, onSelectedIntervalChange = ::onSelectedIntervalChange, + body = MarketsTokenDetailsUM.Body.Loading, + infoBottomSheet = TangemBottomSheetConfig( + isShow = false, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, + ), ), ) private val loadChartJobHolder = JobHolder() init { - loadChart(PriceChangeInterval.H24) + // reload screen if currency changed + modelScope.launch { + currentAppCurrency + .filter { it != params.appCurrency } + .collectLatest { + initialLoad() + } + } + + initialLoad() + } + + private fun initialLoad() { + loadChart(state.value.selectedInterval) + loadInfo() } private fun onSelectedIntervalChange(interval: PriceChangeInterval) { @@ -175,12 +221,60 @@ internal class MarketsTokenDetailsModel @Inject constructor( chartState = it.chartState.copy( status = MarketsTokenDetailsUM.ChartState.Status.ERROR, ), + body = if (it.body is MarketsTokenDetailsUM.Body.Error) { + MarketsTokenDetailsUM.Body.Nothing + } else { + it.body + }, ) } } }.saveIn(loadChartJobHolder) } + private fun loadInfo() { + state.update { + it.copy( + body = MarketsTokenDetailsUM.Body.Loading, + ) + } + + modelScope.launch { + val tokenMarketInfo = getTokenMarketInfoUseCase( + appCurrency = currentAppCurrency.value, + tokenId = params.token.id, + ) + + tokenMarketInfo.fold( + ifRight = { result -> + state.update { + it.copy( + body = MarketsTokenDetailsUM.Body.Content( + description = descriptionConverter.convert(result), + infoBlocks = infoConverter.convert(result), + ), + ) + } + }, + ifLeft = { + state.update { + if (it.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) { + it.copy( + body = MarketsTokenDetailsUM.Body.Error( + onLoadRetryClick = ::onLoadRetryClicked, + ), + ) + } else { + it.copy( + body = MarketsTokenDetailsUM.Body.Nothing, + ) + } + } + }, + ) + } + } + private fun getFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { return when (interval) { PriceChangeInterval.H24 -> { value: BigDecimal -> @@ -251,4 +345,40 @@ internal class MarketsTokenDetailsModel @Inject constructor( MarketChartLook.Type.Falling } } + + private fun showInfoBottomSheet(content: InfoBottomSheetContent) { + state.update { stateToUpdate -> + stateToUpdate.copy( + infoBottomSheet = stateToUpdate.infoBottomSheet.copy( + isShow = true, + onDismissRequest = ::hideInfoBottomSheet, + content = content, + ), + ) + } + } + + private fun hideInfoBottomSheet() { + state.update { stateToUpdate -> + stateToUpdate.copy( + infoBottomSheet = stateToUpdate.infoBottomSheet.copy( + isShow = false, + ), + ) + } + } + + private fun onLoadRetryClicked() { + val currentState = state.value + + if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.ERROR) { + loadChart(currentState.selectedInterval) + } + + if (currentState.body is MarketsTokenDetailsUM.Body.Error || + currentState.body is MarketsTokenDetailsUM.Body.Nothing + ) { + loadInfo() + } + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt new file mode 100644 index 0000000000..40d76daa99 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt @@ -0,0 +1,41 @@ +package com.tangem.features.markets.details.impl.model.converters + +import androidx.compose.runtime.Stable +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent +import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM +import com.tangem.features.markets.impl.R +import com.tangem.utils.converter.Converter + +@Stable +internal class DescriptionConverter( + val onReadModeClicked: (InfoBottomSheetContent) -> Unit, +) : Converter { + + override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.Description? { + return value.shortDescription?.let { desc -> + MarketsTokenDetailsUM.Description( + shortDescription = stringReference(desc), + fullDescription = value.fullDescription?.let { fullDescription -> + stringReference(fullDescription) + }, + onReadMoreClick = { + onReadModeClicked( + InfoBottomSheetContent( + title = resourceReference( + R.string.markets_token_details_about_token_title, + wrappedList( + value.name, + ), + ), + body = stringReference(value.fullDescription ?: ""), + ), + ) + }, + ) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt new file mode 100644 index 0000000000..e90f915acd --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt @@ -0,0 +1,131 @@ +package com.tangem.features.markets.details.impl.model.converters + +import androidx.compose.runtime.Stable +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent +import com.tangem.features.markets.details.impl.ui.state.InfoPointUM +import com.tangem.features.markets.details.impl.ui.state.InsightsUM +import com.tangem.features.markets.impl.R +import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +@Stable +internal class InsightsConverter( + private val appCurrency: Provider, + private val onInfoClick: (InfoBottomSheetContent) -> Unit, +) : Converter { + + override fun convert(value: TokenMarketInfo.Insights): InsightsUM { + return with(value) { + InsightsUM( + h24Info = createInfoPointList( + experiencedBuyerChange = experiencedBuyerChange?.day, + holdersChange = holdersChange?.day, + liquidityChange = liquidityChange?.day, + buyPressureChange = buyPressureChange?.day, + ), + weekInfo = createInfoPointList( + experiencedBuyerChange = experiencedBuyerChange?.week, + holdersChange = holdersChange?.week, + liquidityChange = liquidityChange?.week, + buyPressureChange = buyPressureChange?.week, + ), + monthInfo = createInfoPointList( + experiencedBuyerChange = experiencedBuyerChange?.month, + holdersChange = holdersChange?.month, + liquidityChange = liquidityChange?.month, + buyPressureChange = buyPressureChange?.month, + ), + ) + } + } + + private fun createInfoPointList( + experiencedBuyerChange: BigDecimal?, + holdersChange: BigDecimal?, + liquidityChange: BigDecimal?, + buyPressureChange: BigDecimal?, + ): ImmutableList { + return persistentListOf( + InfoPointUM( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + value = experiencedBuyerChange.convertChange(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_experienced_buyers), + body = resourceReference(R.string.markets_token_details_experienced_buyers_description), + ), + ) + }, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_buy_pressure), + value = buyPressureChange.convertChange(isFiatValue = true), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_buy_pressure), + body = resourceReference(R.string.markets_token_details_buy_pressure_description), + ), + ) + }, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_holders), + value = holdersChange.convertChange(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_holders), + body = resourceReference(R.string.markets_token_details_holders_description), + ), + ) + }, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_liquidity), + value = liquidityChange.convertChange(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_liquidity), + body = resourceReference(R.string.markets_token_details_liquidity_description), + ), + ) + }, + ), + ) + } + + private fun BigDecimal?.convertChange(isFiatValue: Boolean = false): String { + if (this == null) return StringsSigns.DASH_SIGN + + val value = if (isFiatValue) { + val currency = appCurrency() + BigDecimalFormatter.formatCompactFiatAmount( + amount = this.abs(), + fiatCurrencyCode = currency.code, + fiatCurrencySymbol = currency.symbol, + ) + } else { + BigDecimalFormatter.formatCompactAmount(amount = this.abs()) + } + + val spacing = if (isFiatValue) " " else "" + + return when { + this > BigDecimal.ZERO -> StringsSigns.PLUS + spacing + value + this < BigDecimal.ZERO -> StringsSigns.MINUS + spacing + value + this == BigDecimal.ZERO -> value + else -> StringsSigns.DASH_SIGN + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt new file mode 100644 index 0000000000..1b571e168f --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt @@ -0,0 +1,49 @@ +package com.tangem.features.markets.details.impl.model.converters + +import androidx.compose.runtime.Stable +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.features.markets.details.impl.ui.state.LinksUM +import com.tangem.features.markets.impl.R +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList + +@Stable +internal class LinksConverter( + private val onLinkClick: (LinksUM.Link) -> Unit, +) : Converter { + + override fun convert(value: TokenMarketInfo.Links): LinksUM { + return LinksUM( + officialLinks = value.officialLinks?.map { it.convert() }.orEmpty().toImmutableList(), + social = value.social?.map { it.convert() }.orEmpty().toImmutableList(), + repository = value.repository?.map { it.convert() }.orEmpty().toImmutableList(), + blockchainSite = value.blockchainSite?.map { it.convert() }.orEmpty().toImmutableList(), + onLinkClick = onLinkClick, + ) + } + + private fun TokenMarketInfo.Link.convert(): LinksUM.Link { + return LinksUM.Link( + title = stringReference(title), + iconRes = getIconById(id), + url = link, + ) + } + + private fun getIconById(id: String?): Int { + return when (id) { + "linkedin" -> R.drawable.ic_linkedin_24 + "discord" -> R.drawable.ic_discord_24 + "youtube" -> R.drawable.ic_youtube_24 + "telegram" -> R.drawable.ic_telegram_24 + "github" -> R.drawable.ic_github_24 + "twitter" -> R.drawable.ic_twitter_24 + "facebook" -> R.drawable.ic_facebook_24 + "reddit" -> R.drawable.ic_reddit_24 + "instagram" -> R.drawable.ic_instagram_24 + "whitepaper" -> R.drawable.ic_doc_24 + else -> R.drawable.ic_arrow_top_right_24 + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt new file mode 100644 index 0000000000..4bf9f1e111 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt @@ -0,0 +1,135 @@ +package com.tangem.features.markets.details.impl.model.converters + +import androidx.compose.runtime.Stable +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent +import com.tangem.features.markets.details.impl.ui.state.InfoPointUM +import com.tangem.features.markets.details.impl.ui.state.MetricsUM +import com.tangem.features.markets.impl.R +import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Locale + +@Stable +internal class MetricsConverter( + private val appCurrency: Provider, + private val onInfoClick: (InfoBottomSheetContent) -> Unit, +) : Converter { + + @Suppress("LongMethod") + override fun convert(value: TokenMarketInfo.Metrics): MetricsUM { + return with(value) { + MetricsUM( + metrics = persistentListOf( + InfoPointUM( + title = resourceReference(R.string.markets_token_details_market_capitalization), + value = marketCap.formatAmount(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_market_capitalization), + body = resourceReference( + R.string.markets_token_details_market_capitalization_description, + ), + ), + ) + }, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_market_rating), + value = marketRating?.toString() ?: StringsSigns.DASH_SIGN, + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_market_rating), + body = resourceReference(R.string.markets_token_details_market_rating_description), + ), + ) + }, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_trading_volume), + value = volume24h.formatAmount(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_trading_volume), + body = resourceReference( + R.string.markets_token_details_trading_volume_24h_description, + ), + ), + ) + }, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_fully_diluted_valuation), + value = fullyDilutedValuation.formatAmount(), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_fully_diluted_valuation), + body = resourceReference( + R.string.markets_token_details_fully_diluted_valuation_description, + ), + ), + ) + }, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_circulating_supply), + value = circulatingSupply.formatAmount(crypto = true), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_circulating_supply), + body = resourceReference( + R.string.markets_token_details_circulating_supply_description, + ), + ), + ) + }, + ), + InfoPointUM( + title = resourceReference(R.string.markets_token_details_total_supply), + value = totalSupply.formatAmount(crypto = true), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_total_supply), + body = resourceReference(R.string.markets_token_details_total_supply_description), + ), + ) + }, + ), + ), + ) + } + } + + private fun BigDecimal?.formatAmount(crypto: Boolean = false): String { + return if (crypto) { + val formatter = NumberFormat.getNumberInstance(Locale.getDefault()).apply { + maximumFractionDigits = 0 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + formatter.format(this) + } else { + val currency = appCurrency() + BigDecimalFormatter.formatFiatAmount( + fiatAmount = this, + fiatCurrencyCode = currency.code, + fiatCurrencySymbol = currency.symbol, + decimals = 0, + ) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt new file mode 100644 index 0000000000..b08b518ba0 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt @@ -0,0 +1,58 @@ +package com.tangem.features.markets.details.impl.model.converters + +import androidx.compose.runtime.Stable +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM +import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns +import com.tangem.utils.converter.Converter +import java.math.BigDecimal +import java.math.RoundingMode + +@Stable +internal class PricePerformanceConverter( + private val appCurrency: Provider, +) : Converter { + + override fun convert(value: TokenMarketInfo.PricePerformance): PricePerformanceUM { + return PricePerformanceUM( + h24 = value.day.convert(), + month = value.month.convert(), + all = value.allTime.convert(), + ) + } + + private fun TokenMarketInfo.Range?.convert(): PricePerformanceUM.Value { + if (this == null) { + return PricePerformanceUM.Value( + low = StringsSigns.DASH_SIGN, + high = StringsSigns.DASH_SIGN, + indicatorFraction = 0f, + ) + } + + return PricePerformanceUM.Value( + low = low.convert(), + high = high.convert(), + indicatorFraction = calculateFraction(), + ) + } + + private fun BigDecimal?.convert(): String { + val currency = appCurrency() + return BigDecimalFormatter.formatCompactFiatAmount( + amount = this, + fiatCurrencyCode = currency.code, + fiatCurrencySymbol = currency.symbol, + ) + } + + private fun TokenMarketInfo.Range.calculateFraction(): Float { + if (low == null || high == null || low == BigDecimal.ZERO) return 0f + return (high!! - low!!).divide(low!!, RoundingMode.HALF_UP) + .setScale(2, RoundingMode.HALF_UP) + .toFloat().coerceAtMost(1f) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt new file mode 100644 index 0000000000..302aeb7258 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt @@ -0,0 +1,35 @@ +package com.tangem.features.markets.details.impl.model.converters + +import androidx.compose.runtime.Stable +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent +import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM +import com.tangem.features.markets.impl.R +import com.tangem.utils.converter.Converter + +// TODO implement when backend is ready +@Stable +internal class SecurityScoreConverter( + private val onInfoClick: (InfoBottomSheetContent) -> Unit, +) : Converter { + + override fun convert(value: Unit): SecurityScoreUM { + return with(value) { + SecurityScoreUM( + score = 4.7f, + description = "Based on 3 ratings", + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_security_score), + body = stringReference("markets_token_details_security_score_description"), + // FIXME + // resourceReference(R.string.markets_token_details_security_score_description) + ), + ) + }, + ) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt new file mode 100644 index 0000000000..f03cbff9c4 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt @@ -0,0 +1,34 @@ +package com.tangem.features.markets.details.impl.model.converters + +import androidx.compose.runtime.Stable +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent +import com.tangem.features.markets.details.impl.ui.state.LinksUM +import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter + +@Stable +internal class TokenMarketInfoConverter( + appCurrency: Provider, + onInfoClick: (InfoBottomSheetContent) -> Unit, + onLinkClick: (LinksUM.Link) -> Unit, +) : Converter { + + private val insightsConverter = InsightsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick) + private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick) + private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick) + private val pricePerformanceConverter = PricePerformanceConverter(appCurrency = appCurrency) + private val linksConverter = LinksConverter(onLinkClick = onLinkClick) + + override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks { + return MarketsTokenDetailsUM.InformationBlocks( + insights = value.insights?.let { insightsConverter.convert(it) }, + securityScore = securityScoreConverter.convert(Unit), + metrics = value.metrics?.let { metricsConverter.convert(it) }, + pricePerformance = value.pricePerformance?.let { pricePerformanceConverter.convert(it) }, + links = value.links?.let { linksConverter.convert(it) }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index ead8e056f4..7f8dfd5def 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -1,6 +1,8 @@ package com.tangem.features.markets.details.impl.ui +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -12,12 +14,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.MarketChartLook -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.SpacerW4 +import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.marketprice.PriceChangeInPercent @@ -29,9 +30,12 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.disableNestedScroll import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.markets.details.impl.ui.components.InfoBottomSheet import com.tangem.features.markets.details.impl.ui.components.MarketTokenDetailsChart -import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM +import com.tangem.features.markets.details.impl.ui.components.tokenMarketDetailsBody +import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.persistentListOf @@ -44,11 +48,13 @@ internal fun MarketsTokenDetailsContent( modifier: Modifier = Modifier, ) { Content( + modifier = modifier, state = state, onBackClick = onBackClick, onHeaderSizeChange = onHeaderSizeChange, - modifier = modifier, ) + + InfoBottomSheet(config = state.infoBottomSheet) } @Suppress("UnusedPrivateMember") @@ -61,6 +67,7 @@ private fun Content( ) { val backgroundColor = LocalMainBottomSheetColor.current.value val density = LocalDensity.current + val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } Column( modifier = modifier @@ -78,31 +85,46 @@ private fun Content( title = state.tokenName, startButton = TopAppBarButtonUM.Back(onBackClick), ) + SpacerH4() - Header( - state = state, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) + LazyColumn( + modifier = Modifier.disableNestedScroll(), + contentPadding = PaddingValues(bottom = bottomBarHeight), + ) { + item("header") { + Header( + state = state, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) + } + item { SpacerH16() } + item("intervalSelector") { + IntervalSelector( + trendInterval = state.selectedInterval, + onIntervalClick = state.onSelectedIntervalChange, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) + } + item { SpacerH32() } + item( + contentType = "chart", + ) { + MarketTokenDetailsChart( + modifier = Modifier.fillMaxWidth(), + state = state.chartState, + ) + } + item { SpacerH16() } - SpacerH16() - - IntervalSelector( - trendInterval = state.selectedInterval, - onIntervalClick = state.onSelectedIntervalChange, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) - - SpacerH32() - - MarketTokenDetailsChart( - modifier = Modifier.fillMaxWidth(), - state = state.chartState, - ) + tokenMarketDetailsBody( + state = state.body, + ) + } } } @@ -199,6 +221,7 @@ fun PriceChangeInterval.getText(): TextReference { private fun Preview() { TangemThemePreview { Content( + modifier = Modifier.background(TangemTheme.colors.background.tertiary), state = MarketsTokenDetailsUM( tokenName = "Token Name", priceText = "Price", @@ -215,6 +238,12 @@ private fun Preview() { ), selectedInterval = PriceChangeInterval.H24, onSelectedIntervalChange = { }, + body = MarketsTokenDetailsUM.Body.Loading, + infoBottomSheet = TangemBottomSheetConfig( + isShow = false, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, + ), ), onHeaderSizeChange = {}, onBackClick = {}, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt new file mode 100644 index 0000000000..e895b421f5 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/Description.kt @@ -0,0 +1,112 @@ +package com.tangem.features.markets.details.impl.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.extensions.TextReference +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.res.TangemThemePreview +import com.tangem.core.ui.utils.PreviewShimmerContainer +import com.tangem.features.markets.impl.R + +@Composable +internal fun Description( + description: TextReference, + hasFullDescription: Boolean, + onReadMoreClick: () -> Unit, + modifier: Modifier = Modifier, +) { + if (hasFullDescription) { + val text = buildAnnotatedString { + withStyle(SpanStyle(color = TangemTheme.colors.text.secondary)) { + append(description.resolveReference()) + } + withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { + append(" " + stringResource(R.string.common_read_more)) + } + } + + ClickableText( + modifier = modifier, + text = text, + style = TangemTheme.typography.body2, + ) { + text.spanStyles.getOrNull(1)?.let { spanStyle -> + if (it in spanStyle.start..spanStyle.end) { + onReadMoreClick() + } + } + } + } else { + Text( + modifier = modifier, + text = description.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } +} + +@Composable +internal fun DescriptionPlaceholder(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + ) { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + TextShimmer( + modifier = Modifier.fillMaxWidth(fraction = 0.8f), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + } +} + +@Preview +@Composable +private fun ContentPreview() { + TangemThemePreview { + Description( + description = stringReference( + "XRP (XRP) is a cryptocurrency launched in January 2009, where the first " + + "genesis block was mined on 9th January 2009", + ), + hasFullDescription = true, + onReadMoreClick = {}, + ) + } +} + +@Preview +@Composable +private fun PreviewPlaceholder() { + TangemThemePreview { + PreviewShimmerContainer( + actualContent = { + ContentPreview() + }, + shimmerContent = { + DescriptionPlaceholder() + }, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt new file mode 100644 index 0000000000..0d44a41f91 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt @@ -0,0 +1,47 @@ +package com.tangem.features.markets.details.impl.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent + +@Composable +internal fun InfoBottomSheet(config: TangemBottomSheetConfig) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + TangemBottomSheet( + config = config, + skipPartiallyExpanded = false, + addBottomInsets = false, + title = { + TangemBottomSheetTitle(title = it.title) + }, + content = { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(horizontal = TangemTheme.dimens.spacing28), + ) { + Text( + text = it.body.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + SpacerH(bottomBarHeight) + } + }, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt index 5be2fb74a5..09d8317df5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt @@ -1,7 +1,6 @@ package com.tangem.features.markets.details.impl.ui.components import android.content.res.Configuration -import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Text @@ -15,11 +14,10 @@ import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.text.TooltipText import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.res.TangemAnimations import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM +import com.tangem.features.markets.details.impl.ui.state.InfoPointUM @Composable internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) { @@ -42,18 +40,11 @@ internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) overflow = TextOverflow.Ellipsis, ) } - AnimatedContent( - modifier = Modifier, - targetState = infoPointUM.value, - transitionSpec = { TangemAnimations.transitionSpecs.textChange }, - label = "insight block", - ) { - Text( - text = it, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - } + Text( + text = infoPointUM.value, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt index a8cd4bf110..3cd88bc115 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt @@ -20,8 +20,8 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM -import com.tangem.features.markets.details.impl.ui.entity.InsightsUM +import com.tangem.features.markets.details.impl.ui.state.InfoPointUM +import com.tangem.features.markets.details.impl.ui.state.InsightsUM import com.tangem.features.markets.details.impl.ui.getText import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.persistentListOf diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt index 2d968b1327..ca7e4d4390 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.common.ui.charts.downsample.fastForEach +import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.components.SmallButtonShimmer import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.information.InformationBlock @@ -21,7 +21,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.entity.LinksUM +import com.tangem.features.markets.details.impl.ui.state.LinksUM import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -69,8 +69,8 @@ internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) { @OptIn(ExperimentalLayoutApi::class) @Composable private fun SubBlock( - links: ImmutableList, - onLinkClick: (LinksUM.LinkUM) -> Unit, + links: ImmutableList, + onLinkClick: (LinksUM.Link) -> Unit, modifier: Modifier = Modifier, lastBlock: Boolean = false, title: String = "Official links", @@ -166,36 +166,36 @@ private fun ContentPreview() { LinksBlock( state = LinksUM( officialLinks = persistentListOf( - LinksUM.LinkUM( + LinksUM.Link( title = stringReference("Website"), iconRes = R.drawable.ic_plus_24, url = "https://tangem.com", ), - LinksUM.LinkUM( + LinksUM.Link( title = stringReference("Website"), iconRes = R.drawable.ic_plus_24, url = "https://tangem.com", ), - LinksUM.LinkUM( + LinksUM.Link( title = stringReference("Website"), iconRes = R.drawable.ic_plus_24, url = "https://tangem.com", ), ), social = persistentListOf( - LinksUM.LinkUM( + LinksUM.Link( title = stringReference("Twitter"), iconRes = R.drawable.ic_plus_24, url = "https://tangem.com", ), - LinksUM.LinkUM( + LinksUM.Link( title = stringReference("Facebook"), iconRes = R.drawable.ic_plus_24, url = "https://tangem.com", ), ), repository = persistentListOf( - LinksUM.LinkUM( + LinksUM.Link( title = stringReference("Github"), iconRes = R.drawable.ic_plus_24, url = "https://tangem.com", diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt index ee9ce1d5b3..b19876af06 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt @@ -10,11 +10,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import com.tangem.common.ui.charts.MarketChart +import com.tangem.common.ui.charts.getMarketChartBottomAxisHeight import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.rememberMarketChartState import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM +import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData @Composable @@ -34,6 +35,7 @@ internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, mo ) val backgroundColor = LocalMainBottomSheetColor.current.value + val bottomChartAxisHeight = getMarketChartBottomAxisHeight() Box(modifier) { MarketChart( @@ -45,7 +47,8 @@ internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, mo Box( Modifier .drawBehind { drawRect(backgroundColor) } - .matchParentSize(), + .matchParentSize() + .padding(bottom = bottomChartAxisHeight), ) { when (state.status) { MarketsTokenDetailsUM.ChartState.Status.LOADING -> { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt index 20308e8e72..4825c2ab32 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt @@ -17,8 +17,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM -import com.tangem.features.markets.details.impl.ui.entity.MetricsUM +import com.tangem.features.markets.details.impl.ui.state.InfoPointUM +import com.tangem.features.markets.details.impl.ui.state.MetricsUM import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt index 799200dc99..cb3098ca60 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -26,7 +26,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.entity.PricePerformanceUM +import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM import com.tangem.features.markets.details.impl.ui.getText import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.persistentListOf @@ -127,30 +127,18 @@ private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifi modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { - AnimatedContent( - targetState = state.low, - transitionSpec = { TangemAnimations.transitionSpecs.textChange }, - label = "Low price", - ) { - Text( - text = it, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - } + Text( + text = state.low, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) SpacerW8() - AnimatedContent( - targetState = state.high, - transitionSpec = { TangemAnimations.transitionSpecs.textChange }, - label = "High price", - ) { - Text( - text = it, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.End, - ) - } + Text( + text = state.high, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.End, + ) } } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt index 319a08de34..66a75660ef 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt @@ -27,7 +27,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.entity.SecurityScoreUM +import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM import com.tangem.features.markets.impl.R import kotlin.math.round diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt new file mode 100644 index 0000000000..40cc2fc2f5 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt @@ -0,0 +1,142 @@ +package com.tangem.features.markets.details.impl.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM +import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData + +internal fun LazyListScope.tokenMarketDetailsBody(state: MarketsTokenDetailsUM.Body) { + when (state) { + MarketsTokenDetailsUM.Body.Loading -> { + loading() + } + is MarketsTokenDetailsUM.Body.Content -> { + if (state.description != null) { + description(state.description) + } + + infoBlocksList(state.infoBlocks) + } + is MarketsTokenDetailsUM.Body.Error -> { + error(state) + } + MarketsTokenDetailsUM.Body.Nothing -> { + // Do nothing + } + } +} + +private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) { + item("body-error") { + Box(Modifier.fillMaxWidth()) { + UnableToLoadData( + modifier = Modifier + .align(Alignment.Center) + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing40, + ), + onRetryClick = state.onLoadRetryClick, + ) + } + } +} + +private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) { + item("description") { + Description( + modifier = Modifier.blockPaddings(), + description = description.shortDescription, + hasFullDescription = description.fullDescription != null, + onReadMoreClick = description.onReadMoreClick, + ) + } +} + +internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks) { + if (state.insights != null) { + item("insights") { + InsightsBlock( + modifier = Modifier.blockPaddings(), + state = state.insights, + ) + } + } + + if (state.securityScore != null) { + item("securityScore") { + SecurityScoreBlock( + modifier = Modifier.blockPaddings(), + state = state.securityScore, + ) + } + } + + if (state.metrics != null) { + item("metrics") { + MetricsBlock( + modifier = Modifier.blockPaddings(), + state = state.metrics, + ) + } + } + + if (state.pricePerformance != null) { + item("pricePerformance") { + PricePerformanceBlock( + modifier = Modifier.blockPaddings(), + state = state.pricePerformance, + ) + } + } + + if (state.links != null) { + item("links") { + LinksBlock( + modifier = Modifier.blockPaddings(), + state = state.links, + ) + } + } +} + +private fun LazyListScope.loading() { + item("description-loading") { + DescriptionPlaceholder(modifier = Modifier.blockPaddings()) + } + + item("insights-loading") { + InsightsBlockPlaceholder(modifier = Modifier.blockPaddings()) + } + + item("securityScore-loading") { + SecurityScorePlaceHolder(modifier = Modifier.blockPaddings()) + } + + item("metrics-loading") { + MetricsBlockPlaceholder(modifier = Modifier.blockPaddings()) + } + + item("pricePerformance-loading") { + PricePerformanceBlockPlaceholder(modifier = Modifier.blockPaddings()) + } + + item("links-loading") { + LinksBlockPlaceholder(modifier = Modifier.blockPaddings()) + } +} + +@Composable +private fun Modifier.blockPaddings(): Modifier { + return this.padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing12, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InsightsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InsightsUM.kt deleted file mode 100644 index 9c240b9214..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InsightsUM.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.entity - -import androidx.compose.runtime.Immutable -import kotlinx.collections.immutable.PersistentList - -@Immutable -internal data class InsightsUM( - val h24Info: PersistentList, - val weekInfo: PersistentList, - val monthInfo: PersistentList, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/LinksUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/LinksUM.kt deleted file mode 100644 index e22571424f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/LinksUM.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.entity - -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference -import kotlinx.collections.immutable.PersistentList - -@Immutable -internal data class LinksUM( - val officialLinks: PersistentList, - val social: PersistentList, - val repository: PersistentList, - val blockchainSite: PersistentList, - val onLinkClick: (LinkUM) -> Unit, -) { - @Immutable - data class LinkUM( - @DrawableRes val iconRes: Int, - val title: TextReference, - val url: String, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt new file mode 100644 index 0000000000..858fc017c5 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt @@ -0,0 +1,9 @@ +package com.tangem.features.markets.details.impl.ui.state + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference + +internal data class InfoBottomSheetContent( + val title: TextReference, + val body: TextReference, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InfoPointUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt similarity index 62% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InfoPointUM.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt index 44816c5ab1..5ef1b1ad91 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/InfoPointUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt @@ -1,9 +1,7 @@ -package com.tangem.features.markets.details.impl.ui.entity +package com.tangem.features.markets.details.impl.ui.state -import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference -@Immutable internal data class InfoPointUM( val title: TextReference, val value: String, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt new file mode 100644 index 0000000000..f7d33a8ddc --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.markets.details.impl.ui.state + +import kotlinx.collections.immutable.ImmutableList + +internal data class InsightsUM( + val h24Info: ImmutableList, + val weekInfo: ImmutableList, + val monthInfo: ImmutableList, +) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt new file mode 100644 index 0000000000..d1a6d13860 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt @@ -0,0 +1,19 @@ +package com.tangem.features.markets.details.impl.ui.state + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +internal data class LinksUM( + val officialLinks: ImmutableList, + val social: ImmutableList, + val repository: ImmutableList, + val blockchainSite: ImmutableList, + val onLinkClick: (Link) -> Unit, +) { + data class Link( + @DrawableRes val iconRes: Int, + val title: TextReference, + val url: String, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt similarity index 64% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt index 713d890bfb..42f7c3e9e0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MarketsTokenDetailsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt @@ -1,14 +1,14 @@ -package com.tangem.features.markets.details.impl.ui.entity +package com.tangem.features.markets.details.impl.ui.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.markets.PriceChangeInterval import java.math.BigDecimal -@Immutable internal data class MarketsTokenDetailsUM( val tokenName: String, val priceText: String, @@ -19,10 +19,10 @@ internal data class MarketsTokenDetailsUM( val selectedInterval: PriceChangeInterval, val chartState: ChartState, val onSelectedIntervalChange: (PriceChangeInterval) -> Unit, - // val info : Information TODO [REDACTED_TASK_KEY] + val infoBottomSheet: TangemBottomSheetConfig, + val body: Body, ) { - @Immutable data class ChartState( val status: Status, val dataProducer: MarketChartDataProducer, @@ -30,18 +30,39 @@ internal data class MarketsTokenDetailsUM( val onLoadRetryClick: () -> Unit, val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit, ) { - @Immutable enum class Status { LOADING, ERROR, DATA } } - @Immutable - data class Information( + data class InformationBlocks( val insights: InsightsUM?, val securityScore: SecurityScoreUM?, val metrics: MetricsUM?, val pricePerformance: PricePerformanceUM?, val links: LinksUM?, ) + + @Immutable + sealed interface Body { + + data class Error( + val onLoadRetryClick: () -> Unit, + ) : Body + + data object Loading : Body + + data class Content( + val description: Description?, + val infoBlocks: InformationBlocks, + ) : Body + + data object Nothing : Body + } + + data class Description( + val shortDescription: TextReference, + val fullDescription: TextReference?, + val onReadMoreClick: () -> Unit, + ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MetricsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt similarity index 54% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MetricsUM.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt index 14f5a9fb86..b1982093d4 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/MetricsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt @@ -1,9 +1,7 @@ -package com.tangem.features.markets.details.impl.ui.entity +package com.tangem.features.markets.details.impl.ui.state -import androidx.compose.runtime.Immutable import kotlinx.collections.immutable.PersistentList -@Immutable internal data class MetricsUM( val metrics: PersistentList, ) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/PricePerformanceUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt similarity index 70% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/PricePerformanceUM.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt index 194354a363..e56e10312e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/PricePerformanceUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt @@ -1,15 +1,12 @@ -package com.tangem.features.markets.details.impl.ui.entity +package com.tangem.features.markets.details.impl.ui.state import androidx.annotation.FloatRange -import androidx.compose.runtime.Immutable -@Immutable internal data class PricePerformanceUM( val h24: Value, val month: Value, val all: Value, ) { - @Immutable data class Value( val low: String, val high: String, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/SecurityScoreUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt similarity index 63% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/SecurityScoreUM.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt index 4afa252dba..35f35d4b9c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/entity/SecurityScoreUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt @@ -1,9 +1,7 @@ -package com.tangem.features.markets.details.impl.ui.entity +package com.tangem.features.markets.details.impl.ui.state import androidx.annotation.FloatRange -import androidx.compose.runtime.Immutable -@Immutable internal data class SecurityScoreUM( @FloatRange(from = 0.0, to = 5.0) val score: Float, val description: String, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index 4e4d85ea7f..167b03fd6c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -11,9 +11,9 @@ import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.component.BottomSheetState import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager -import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM -import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM -import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM +import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -200,10 +200,12 @@ internal class MarketsListModel @Inject constructor( modelScope.launch { marketsListUMStateManager.searchQueryFlow - .filter { it.isNotEmpty() } .debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS) .distinctUntilChanged() - .filter { activeListManager == searchMarketsListManager } + .onEach { + if (it.isEmpty()) searchMarketsListManager.clearStateAndStopAllActions() + } + .filter { it.isNotEmpty() && activeListManager == searchMarketsListManager } .collectLatest { searchMarketsListManager.reload(searchText = it) } @@ -229,6 +231,7 @@ internal class MarketsListModel @Inject constructor( } } } + private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) { launch { while (true) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index f19fc26c78..590aae7f3f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -7,8 +7,8 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket -import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM -import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM.TrendInterval +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @@ -70,10 +70,11 @@ internal class MarketsTokenItemConverter( private fun TokenMarket.getMarketCap(): String? { val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null - return BigDecimalFormatter.formatCompactAmount( - value, + return BigDecimalFormatter.formatCompactFiatAmount( + amount = value, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, + threeDigitsMethod = true, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt index d84184d89a..bc5dcaed36 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt @@ -6,9 +6,9 @@ import com.tangem.features.markets.tokenlist.impl.model.converters.MarketsTokenI import com.tangem.features.markets.tokenlist.impl.model.utils.logAction import com.tangem.features.markets.tokenlist.impl.model.utils.logStatus import com.tangem.features.markets.tokenlist.impl.model.utils.logUpdateResults -import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM -import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM.TrendInterval -import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval +import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import com.tangem.pagination.Batch import com.tangem.pagination.BatchAction import com.tangem.pagination.BatchFetchResult diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index fabc2afbfd..806d5434cb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -7,11 +7,11 @@ import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.markets.impl.R -import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByBottomSheetContentUM -import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM -import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM -import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM -import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM +import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM +import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM +import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 56f0361cff..6dcb360962 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -38,10 +38,10 @@ import com.tangem.features.markets.component.BottomSheetState import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet -import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM -import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM -import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByBottomSheetContentUM -import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM +import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM +import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM +import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt index 14873a4e8d..7658893fb7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItem.kt @@ -46,7 +46,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.windowsize.WindowSizeType import com.tangem.features.markets.impl.R -import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider import com.tangem.utils.StringsSigns.MINUS import kotlinx.coroutines.launch diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index 57aa98cc35..0da161d62a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -9,10 +9,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.buttons.SecondarySmallButton @@ -20,8 +16,9 @@ import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.disableNestedScroll import com.tangem.features.markets.impl.R -import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM +import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import kotlinx.coroutines.launch private const val LOAD_NEXT_PAGE_ON_END_INDEX = 50 @@ -54,7 +51,7 @@ internal fun MarketsListLazyColumn( if (state is ListUM.Loading) { LazyColumn( - modifier = Modifier.nestedScroll(DisableParentConnection), + modifier = Modifier.disableNestedScroll(), state = rememberLazyListState(), contentPadding = PaddingValues(bottom = bottomBarHeight), userScrollEnabled = false, @@ -65,7 +62,7 @@ internal fun MarketsListLazyColumn( } } else { LazyColumn( - modifier = modifier.nestedScroll(DisableParentConnection), + modifier = modifier.disableNestedScroll(), state = lazyListState, contentPadding = PaddingValues(bottom = bottomBarHeight), userScrollEnabled = true, @@ -221,10 +218,4 @@ fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buf emitted = onLoadMore() } } -} - -private object DisableParentConnection : NestedScrollConnection { - override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { - return available.copy(x = 0f) - } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt index 67fe12a9c3..b5c0966102 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt @@ -19,8 +19,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.impl.R -import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByBottomSheetContentUM -import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM +import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM +import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM @Composable fun MarketsListSortByBottomSheet(config: TangemBottomSheetConfig) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt index 5cc524cd87..41d1596f68 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt @@ -4,7 +4,7 @@ package com.tangem.features.markets.tokenlist.impl.ui.preview import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM +import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import kotlinx.collections.immutable.persistentListOf internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListItemUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt similarity index 94% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListItemUM.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt index 3dedf931c4..020dd5b37a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListItemUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.tokenlist.impl.ui.entity +package com.tangem.features.markets.tokenlist.impl.ui.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartLook diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt similarity index 97% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListUM.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt index 39d29633d4..7906cc1fb9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/MarketsListUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.tokenlist.impl.ui.entity +package com.tangem.features.markets.tokenlist.impl.ui.state import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/SortByBottomSheetContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/SortByBottomSheetContentUM.kt similarity index 79% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/SortByBottomSheetContentUM.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/SortByBottomSheetContentUM.kt index fc3354f4b9..f3b4e9682b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/entity/SortByBottomSheetContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/SortByBottomSheetContentUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.tokenlist.impl.ui.entity +package com.tangem.features.markets.tokenlist.impl.ui.state import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent From a25cbc8d24a7ac3fb4a0b2a575fdbe6b3f618bca Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 8 Aug 2024 18:29:44 +0300 Subject: [PATCH 41/87] Updated on 2026-08-14 --- .../api/common/SwitchBaseUrlInterceptor.kt | 43 +++++++++++ .../config/managers/DevApiConfigsManager.kt | 69 +++++++++++++++++ .../config/managers/ProdApiConfigsManager.kt | 18 +++++ .../local/preferences/PreferencesKeys.kt | 2 + .../managers/ProdApiConfigsManagerTest.kt | 77 +++++++++++++++++++ 5 files changed, 209 insertions(+) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchBaseUrlInterceptor.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt create mode 100644 core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchBaseUrlInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchBaseUrlInterceptor.kt new file mode 100644 index 0000000000..6da54754b1 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchBaseUrlInterceptor.kt @@ -0,0 +1,43 @@ +package com.tangem.datasource.api.common + +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor +import okhttp3.Response +import okio.IOException + +/** + * Switch base url [Interceptor] + * + * @property id api config id [ApiConfig.ID] + * @property apiConfigsManager api configs manager + * +[REDACTED_AUTHOR] + */ +internal class SwitchBaseUrlInterceptor( + private val id: ApiConfig.ID, + private val apiConfigsManager: ApiConfigsManager, +) : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + val builder = request.newBuilder() + + request = builder + .url(url = request.url.adjustBaseUrl()) + .build() + + return chain.proceed(request) + } + + private fun HttpUrl.adjustBaseUrl(): HttpUrl { + val host = apiConfigsManager.getBaseUrl(id).toHttpUrl().host + + return this.newBuilder() + .host(host) + .build() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt new file mode 100644 index 0000000000..0af38d7ba9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt @@ -0,0 +1,69 @@ +package com.tangem.datasource.api.common.config.managers + +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +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.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +/** + * Implementation of [ApiConfigsManager] in DEV environment + * + * @property appPreferencesStore app preferences store + * @property dispatchers coroutine dispatcher provider + */ +internal class DevApiConfigsManager( + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : MutableApiConfigsManager { + + override val configs: Flow> get() = _apiConfigs + + private val _apiConfigs = MutableStateFlow(value = ApiConfig.values()) + + override suspend fun initialize() { + // We can't use appPreferencesStore.getObjectMap as base flow, + // because we should keep possibility to work with configs synchronous. + // See [getBaseUrl] + appPreferencesStore.getObjectMap(PreferencesKeys.apiConfigsEnvironmentKey) + .onEach { savedEnvironments -> + _apiConfigs.value = _apiConfigs.value.map { config -> + val savedEnvironment = savedEnvironments[config.id.name] + + if (savedEnvironment != null) { + config.copySealed(currentEnvironment = savedEnvironment) + } else { + config + } + } + } + .launchIn(CoroutineScope(dispatchers.main)) + } + + override fun getBaseUrl(id: ApiConfig.ID): String { + val config = _apiConfigs.value.firstOrNull { it.id == id } + ?: error("Api config with id [$id] not found. Check ApiConfig implementations") + + return config.environments[config.currentEnvironment] + ?: error( + "Api config with id [$id] doesn't contain environment [${config.currentEnvironment}]. " + + "Check ApiConfig implementations", + ) + } + + override suspend fun changeEnvironment(id: String, environment: ApiEnvironment) { + appPreferencesStore.editData { mutablePreferences -> + val updatedMap = mutablePreferences.getObjectMap(PreferencesKeys.apiConfigsEnvironmentKey) + .toMutableMap() + .apply { put(id, environment) } + + mutablePreferences.setObjectMap(PreferencesKeys.apiConfigsEnvironmentKey, updatedMap) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt new file mode 100644 index 0000000000..9f5e1e31e5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.api.common.config.managers + +import com.tangem.datasource.api.common.config.ApiConfig + +/** Implementation of [ApiConfigsManager] in PROD environment */ +internal class ProdApiConfigsManager : ApiConfigsManager { + + override fun getBaseUrl(id: ApiConfig.ID): String { + val config = ApiConfig.values().firstOrNull { it.id == id } + ?: error("Api config with id [$id] not found. Check ApiConfig implementations") + + return config.environments[config.currentEnvironment] + ?: error( + "Api config with id [$id] doesn't contain " + + "environment [${config.currentEnvironment}]. Check ApiConfig implementations", + ) + } +} \ 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 bb2c5fa1b0..9b01024cc8 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 @@ -95,6 +95,8 @@ object PreferencesKeys { booleanPreferencesKey(name = "isTokenSwapPromoOkxShown") } + val apiConfigsEnvironmentKey by lazy { stringPreferencesKey(name = "apiConfigsEnvironment") } + // region Permission fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission") diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt new file mode 100644 index 0000000000..bfc7de29cc --- /dev/null +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -0,0 +1,77 @@ +package com.tangem.datasource.api.common.config.managers + +import com.google.common.truth.Truth +import com.tangem.datasource.BuildConfig +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiConfig.Companion.DEBUG_BUILD_TYPE +import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUILD_TYPE +import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE +import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE +import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE +import com.tangem.datasource.api.common.config.Express +import com.tangem.datasource.api.common.config.TangemTech +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** +[REDACTED_AUTHOR] + */ +@RunWith(Parameterized::class) +internal class ProdApiConfigsManagerTest(private val model: Model) { + + private val manager = ProdApiConfigsManager() + + @Test + fun test_getBaseUrl() { + val actual = manager.getBaseUrl(id = model.id) + + Truth.assertThat(actual).isEqualTo(model.expected) + } + + data class Model(val id: ApiConfig.ID, val expected: String) + + private companion object { + + @JvmStatic + @Parameterized.Parameters + fun data(): Collection = ApiConfig.values().map { + when (it) { + is Express -> createExpressModel() + is TangemTech -> createTangemTechModel() + } + } + + private fun createExpressModel(): Model { + return Model( + id = ApiConfig.ID.Express, + expected = when (BuildConfig.BUILD_TYPE) { + DEBUG_BUILD_TYPE -> "[REDACTED_ENV_URL]" + INTERNAL_BUILD_TYPE, + MOCKED_BUILD_TYPE, + -> "[REDACTED_ENV_URL]" + EXTERNAL_BUILD_TYPE, + RELEASE_BUILD_TYPE, + -> "https://express.tangem.com/v1/" + else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") + }, + ) + } + + private fun createTangemTechModel(): Model { + return Model( + id = ApiConfig.ID.TangemTech, + expected = when (BuildConfig.BUILD_TYPE) { + DEBUG_BUILD_TYPE, + INTERNAL_BUILD_TYPE, + -> "https://devapi.tangem-tech.com/v1/" + MOCKED_BUILD_TYPE, + EXTERNAL_BUILD_TYPE, + RELEASE_BUILD_TYPE, + -> "https://api.tangem-tech.com/v1/" + else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") + }, + ) + } + } +} \ No newline at end of file From c63e6daf3c49c04e78e00a0dda3440b13cf2c3a3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 8 Aug 2024 19:22:21 +0300 Subject: [PATCH 42/87] Updated on 2026-08-14 --- .../tangem/tap/features/main/MainViewModel.kt | 6 +- .../com/tangem/datasource/di/NetworkModule.kt | 68 +++++++++++++++---- .../tangem/datasource/utils/HttpClientExt.kt | 27 +++++++- 3 files changed, 85 insertions(+), 16 deletions(-) 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 60ea643487..c108d54aab 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 @@ -7,6 +7,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -43,8 +44,9 @@ internal class MainViewModel @Inject constructor( private val walletManagersFacade: WalletManagersFacade, private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, - private val stakingFeatureToggles: StakingFeatureToggles, + stakingFeatureToggles: StakingFeatureToggles, private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, + private val apiConfigsManager: ApiConfigsManager, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -82,6 +84,8 @@ internal class MainViewModel @Inject constructor( /** Loading the resources needed to run the application */ private fun loadApplicationResources() { viewModelScope.launch(dispatchers.main) { + apiConfigsManager.initialize() + blockchainSDKFactory.init() prepareSelectedWalletFeedback() diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 6951732450..19a3e4c6b1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -3,6 +3,10 @@ package com.tangem.datasource.di import android.content.Context import com.squareup.moshi.Moshi import com.tangem.datasource.BuildConfig +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager +import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager +import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.markets.TangemTechMarketsApi @@ -10,12 +14,15 @@ import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApiV2 import com.tangem.datasource.api.tangemTech.TangemTechServiceApi +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.utils.RequestHeader import com.tangem.datasource.utils.RequestHeader.* +import com.tangem.datasource.utils.addEnvironmentSwitcher import com.tangem.datasource.utils.addHeaders import com.tangem.datasource.utils.addLoggers import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.version.AppVersionProvider import dagger.Module import dagger.Provides @@ -32,6 +39,19 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) class NetworkModule { + @Provides + @Singleton + fun provideApiConfigManager( + appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, + ): ApiConfigsManager { + return if (BuildConfig.TESTER_MENU_ENABLED) { + DevApiConfigsManager(appPreferencesStore, dispatchers) + } else { + ProdApiConfigsManager() + } + } + @Provides @Singleton fun provideExpressApi( @@ -39,18 +59,15 @@ class NetworkModule { @ApplicationContext context: Context, expressAuthProvider: ExpressAuthProvider, appVersionProvider: AppVersionProvider, + apiConfigsManager: ApiConfigsManager, ): TangemExpressApi { - val url = if (BuildConfig.ENVIRONMENT == "dev") { - STAGE_EXPRESS_BASE_URL - } else { - PROD_EXPRESS_BASE_URL - } return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .baseUrl(url) + .baseUrl(apiConfigsManager.getBaseUrl(id = ApiConfig.ID.Express)) .client( OkHttpClient.Builder() + .addEnvironmentSwitcher(ApiConfig.ID.Express, apiConfigsManager) .addHeaders(Express(expressAuthProvider)) .addHeaders(AppVersionPlatformHeaders(appVersionProvider)) .addLoggers(context) @@ -87,8 +104,15 @@ class NetworkModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, appVersionProvider: AppVersionProvider, + apiConfigsManager: ApiConfigsManager, ): TangemTechApi { - return provideTangemTechApiInternal(moshi, context, appVersionProvider, PROD_V1_TANGEM_TECH_BASE_URL) + return provideTangemTechApiInternal( + moshi = moshi, + context = context, + appVersionProvider = appVersionProvider, + apiConfigsManager = apiConfigsManager, + baseUrl = apiConfigsManager.getBaseUrl(id = ApiConfig.ID.TangemTech), + ) } @Provides @@ -97,8 +121,15 @@ class NetworkModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, appVersionProvider: AppVersionProvider, + apiConfigsManager: ApiConfigsManager, ): TangemTechApiV2 { - return provideTangemTechApiInternal(moshi, context, appVersionProvider, PROD_V2_TANGEM_TECH_BASE_URL) + return provideTangemTechApiInternal( + moshi = moshi, + context = context, + appVersionProvider = appVersionProvider, + apiConfigsManager = apiConfigsManager, + baseUrl = PROD_V2_TANGEM_TECH_BASE_URL, + ) } @Provides @@ -108,8 +139,15 @@ class NetworkModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, appVersionProvider: AppVersionProvider, + apiConfigsManager: ApiConfigsManager, ): TangemTechApi { - return provideTangemTechApiInternal(moshi, context, appVersionProvider, DEV_V1_TANGEM_TECH_BASE_URL) + return provideTangemTechApiInternal( + moshi = moshi, + context = context, + appVersionProvider = appVersionProvider, + apiConfigsManager = apiConfigsManager, + baseUrl = DEV_V1_TANGEM_TECH_BASE_URL, + ) } @Provides @@ -118,12 +156,14 @@ class NetworkModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, appVersionProvider: AppVersionProvider, + apiConfigsManager: ApiConfigsManager, ): TangemTechServiceApi { return provideTangemTechApiInternal( moshi = moshi, context = context, appVersionProvider = appVersionProvider, - baseUrl = PROD_V1_TANGEM_TECH_BASE_URL, + apiConfigsManager = apiConfigsManager, + baseUrl = apiConfigsManager.getBaseUrl(id = ApiConfig.ID.TangemTech), timeouts = Timeouts( callTimeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS, ), @@ -138,11 +178,13 @@ class NetworkModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, appVersionProvider: AppVersionProvider, + apiConfigsManager: ApiConfigsManager, ): TangemTechMarketsApi { return provideTangemTechApiInternal( moshi = moshi, context = context, appVersionProvider = appVersionProvider, + apiConfigsManager = apiConfigsManager, baseUrl = DEV_V1_TANGEM_TECH_BASE_URL, timeouts = Timeouts( callTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, @@ -157,11 +199,13 @@ class NetworkModule { moshi: Moshi, context: Context, appVersionProvider: AppVersionProvider, + apiConfigsManager: ApiConfigsManager, baseUrl: String, timeouts: Timeouts = Timeouts(), requestHeaders: List = listOf(CacheControlHeader, AppVersionPlatformHeaders(appVersionProvider)), ): T { val client = OkHttpClient.Builder() + .addEnvironmentSwitcher(id = ApiConfig.ID.TangemTech, apiConfigsManager = apiConfigsManager) .let { builder -> var b = builder if (timeouts.callTimeoutSeconds != null) { @@ -204,13 +248,9 @@ class NetworkModule { private companion object { const val STAKEKIT_BASE_URL = "https://api.stakek.it/v1/" - const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/" - const val STAGE_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]" - const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]" const val DEV_V1_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/" - const val PROD_V1_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/" const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/" const val TANGEM_TECH_SERVICE_TIMEOUT_SECONDS = 5L diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index 5490d5f8f3..0f571dd13a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -3,6 +3,9 @@ package com.tangem.datasource.utils import android.content.Context import com.chuckerteam.chucker.api.ChuckerInterceptor import com.tangem.datasource.BuildConfig +import com.tangem.datasource.api.common.SwitchBaseUrlInterceptor +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import okhttp3.Interceptor import okhttp3.OkHttpClient @@ -25,7 +28,7 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade /** * Extension for logging each [OkHttpClient] request * - * @param level logging level. By default, only the request body. + * @param context context */ internal fun OkHttpClient.Builder.addLoggers(context: Context? = null): OkHttpClient.Builder { return if (BuildConfig.LOG_ENABLED) { @@ -36,4 +39,26 @@ internal fun OkHttpClient.Builder.addLoggers(context: Context? = null): OkHttpCl } else { this } +} + +/** + * Add environment switcher + * + * @param id class of [ApiConfig] + * @param apiConfigsManager api configs manager + */ +internal fun OkHttpClient.Builder.addEnvironmentSwitcher( + id: ApiConfig.ID, + apiConfigsManager: ApiConfigsManager, +): OkHttpClient.Builder { + return if (BuildConfig.TESTER_MENU_ENABLED) { + addInterceptor( + interceptor = SwitchBaseUrlInterceptor( + id = id, + apiConfigsManager = apiConfigsManager, + ), + ) + } else { + this + } } \ No newline at end of file From 6aa15f23227c6710630f27973d95452b615c0255 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 9 Aug 2024 12:25:31 +0300 Subject: [PATCH 43/87] Updated on 2026-08-14 --- features/tester/impl/build.gradle.kts | 4 + .../tester/presentation/TesterActivity.kt | 20 +- .../state/EnvironmentTogglesScreenUM.kt | 37 ++++ .../ui/EnvironmentTogglesScreen.kt | 181 ++++++++++++++++++ .../EnvironmentsTogglesViewModel.kt | 95 +++++++++ .../state/FeatureTogglesContentState.kt | 3 +- .../featuretoggles/ui/FeatureTogglesScreen.kt | 12 +- .../viewmodels/FeatureTogglesViewModel.kt | 5 +- .../menu/state/TesterMenuContentState.kt | 8 +- .../presentation/menu/ui/TesterMenuScreen.kt | 18 +- .../presentation/navigation/TesterScreen.kt | 2 +- .../impl/src/main/res/values/strings.xml | 2 +- 12 files changed, 364 insertions(+), 23 deletions(-) create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/state/EnvironmentTogglesScreenUM.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 01480f6da6..0e94a8abeb 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { /** Compose */ implementation(deps.compose.material) + implementation(deps.compose.material3) implementation(deps.compose.foundation) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) @@ -33,9 +34,11 @@ dependencies { /** Other libraries */ implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) implementation(deps.timber) /** Core modules */ + implementation(projects.core.datasource) implementation(projects.core.featuretoggles) implementation(projects.core.ui) implementation(projects.core.utils) @@ -45,5 +48,6 @@ dependencies { implementation(projects.features.tester.api) /** Other modules */ + implementation(projects.common.routing) implementation(projects.libs.crypto) } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 62206ea5e5..10d761ffb9 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -1,18 +1,22 @@ package com.tangem.feature.tester.presentation import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeActivity import com.tangem.feature.tester.presentation.actions.TesterActionsScreen import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel +import com.tangem.feature.tester.presentation.environments.ui.EnvironmentTogglesScreen +import com.tangem.feature.tester.presentation.environments.viewmodels.EnvironmentsTogglesViewModel import com.tangem.feature.tester.presentation.featuretoggles.ui.FeatureTogglesScreen import com.tangem.feature.tester.presentation.featuretoggles.viewmodels.FeatureTogglesViewModel import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState @@ -37,6 +41,9 @@ internal class TesterActivity : ComposeActivity() { @Inject lateinit var appFinisher: AppFinisher + @Inject + lateinit var appRouter: AppRouter + private val innerTesterRouter: InnerTesterRouter get() = requireNotNull(testerRouter as? InnerTesterRouter) { "TesterRouter must be InnerTesterRouter for tester feature" @@ -60,8 +67,9 @@ internal class TesterActivity : ComposeActivity() { composable(route = TesterScreen.MENU.name) { TesterMenuScreen( state = TesterMenuContentState( - onBackClick = innerTesterRouter::back, + onBackClick = { appRouter.pop { finish() } }, onFeatureTogglesClick = { innerTesterRouter.open(TesterScreen.FEATURE_TOGGLES) }, + onEnvironmentTogglesClick = { innerTesterRouter.open(TesterScreen.ENVIRONMENTS_TOGGLES) }, onTesterActionsClick = { innerTesterRouter.open(TesterScreen.TESTER_ACTIONS) }, ), ) @@ -75,6 +83,16 @@ internal class TesterActivity : ComposeActivity() { FeatureTogglesScreen(state = viewModel.uiState) } + composable(route = TesterScreen.ENVIRONMENTS_TOGGLES.name) { + val viewModel = hiltViewModel().apply { + setupNavigation(innerTesterRouter) + } + + EnvironmentTogglesScreen( + uiModel = viewModel.uiState.collectAsState().value, + ) + } + composable(route = TesterScreen.TESTER_ACTIONS.name) { val viewModel = hiltViewModel().apply { setupNavigation(innerTesterRouter) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/state/EnvironmentTogglesScreenUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/state/EnvironmentTogglesScreenUM.kt new file mode 100644 index 0000000000..a11c9dbac0 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/state/EnvironmentTogglesScreenUM.kt @@ -0,0 +1,37 @@ +package com.tangem.feature.tester.presentation.environments.state + +import androidx.annotation.StringRes +import kotlinx.collections.immutable.ImmutableSet + +/** + * Content state of environment toggles screen + * + * @property title title + * @property apiInfoList environment toggles list + * @property onEnvironmentSelect the lambda to be invoked when button is pressed + * @property onBackClick the lambda to be invoked when back button is pressed + * @property onApplyChangesClick the lambda to be invoked when apply changes button is pressed + */ +internal data class EnvironmentTogglesScreenUM( + @StringRes val title: Int, + val apiInfoList: ImmutableSet, + val onEnvironmentSelect: (id: String, environment: String) -> Unit, + val onBackClick: () -> Unit, + val onApplyChangesClick: () -> Unit, +) { + + /** + * Api info + * + * @property name api name + * @property select select environment + * @property url select url + * @property environments list of environments + */ + data class ApiInfoUM( + val name: String, + val select: String, + val url: String, + val environments: ImmutableSet, + ) +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt new file mode 100644 index 0000000000..d26480798f --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt @@ -0,0 +1,181 @@ +package com.tangem.feature.tester.presentation.environments.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.environments.state.EnvironmentTogglesScreenUM +import kotlinx.collections.immutable.persistentSetOf + +/** + * Screen with environment toggles list + * + * @param uiModel screen state + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun EnvironmentTogglesScreen(uiModel: EnvironmentTogglesScreenUM) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary), + ) { + stickyHeader { + AppBarWithBackButton( + onBackClick = uiModel.onBackClick, + text = stringResource(id = uiModel.title), + ) + } + + itemsIndexed( + items = uiModel.apiInfoList.toTypedArray(), + key = { _, info -> info.name }, + ) { index, info -> + EnvironmentButtons( + uiModel = info, + onSelect = { isChange -> uiModel.onEnvironmentSelect(info.name, isChange) }, + modifier = Modifier.padding( + bottom = if (index == uiModel.apiInfoList.toTypedArray().lastIndex) { + TangemTheme.dimens.spacing0 + } else { + TangemTheme.dimens.spacing10 + }, + ), + ) + } + + item { + PrimaryButton( + text = stringResource(id = R.string.apply_changes), + onClick = uiModel.onApplyChangesClick, + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing16), + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun EnvironmentButtons( + uiModel: EnvironmentTogglesScreenUM.ApiInfoUM, + onSelect: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), + ) { + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = uiModel.name, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + style = TangemTheme.typography.h3, + ) + + AnimatedContent(targetState = uiModel.url, label = "") { + Text( + text = it, + color = TangemTheme.colors.text.accent, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.body2, + ) + } + } + + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + uiModel.environments.onEachIndexed { index, environment -> + key(environment) { + SegmentedButton( + selected = environment == uiModel.select, + onClick = { onSelect(environment) }, + shape = when (index) { + 0 -> RoundedCornerShape( + topStart = TangemTheme.dimens.radius12, + bottomStart = TangemTheme.dimens.radius12, + ) + uiModel.environments.toTypedArray().lastIndex -> RoundedCornerShape( + topEnd = TangemTheme.dimens.radius12, + bottomEnd = TangemTheme.dimens.radius12, + ) + else -> RectangleShape + }, + colors = SegmentedButtonDefaults.colors( + activeContainerColor = TangemTheme.colors.control.checked, + activeContentColor = TangemTheme.colors.text.primary2, + inactiveContainerColor = TangemTheme.colors.control.unchecked, + inactiveContentColor = TangemTheme.colors.text.primary1, + ), + border = BorderStroke(0.dp, TangemTheme.colors.background.tertiary), + ) { + Text(text = environment, style = TangemTheme.typography.subtitle2) + } + } + } + } + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewFeatureTogglesScreen() { + TangemThemePreview { + var select by remember { mutableStateOf(ApiEnvironment.DEV.name) } + + EnvironmentTogglesScreen( + uiModel = EnvironmentTogglesScreenUM( + title = R.string.environment_toggles, + apiInfoList = persistentSetOf( + EnvironmentTogglesScreenUM.ApiInfoUM( + name = ApiConfig.ID.Express.name, + select = select, + url = "https://api.express.tangem.com", + environments = persistentSetOf( + ApiEnvironment.DEV.name, + ApiEnvironment.STAGE.name, + ApiEnvironment.PROD.name, + ), + ), + EnvironmentTogglesScreenUM.ApiInfoUM( + name = ApiConfig.ID.TangemTech.name, + select = select, + url = "https://api.express.tangem.com", + environments = persistentSetOf( + ApiEnvironment.DEV.name, + ApiEnvironment.PROD.name, + ), + ), + ), + onEnvironmentSelect = { _: String, s1: String -> select = s1 }, + onBackClick = {}, + onApplyChangesClick = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt new file mode 100644 index 0000000000..1b4b896c3e --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt @@ -0,0 +1,95 @@ +package com.tangem.feature.tester.presentation.environments.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager +import com.tangem.datasource.api.common.config.managers.MutableApiConfigsManager +import com.tangem.feature.tester.impl.BuildConfig +import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.environments.state.EnvironmentTogglesScreenUM +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableSet +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * ViewModel for screen with list of environment toggles + * + * @param apiConfigsManager manager for getting information about the api configs + * +[REDACTED_AUTHOR] + */ +@HiltViewModel +internal class EnvironmentsTogglesViewModel @Inject constructor( + apiConfigsManager: ApiConfigsManager, +) : ViewModel() { + + /** Current ui state */ + val uiState: StateFlow + get() = _uiState + + private val _uiState = MutableStateFlow(value = getInitialState()) + + private val mutableApiConfigsManager: MutableApiConfigsManager = + requireNotNull(apiConfigsManager as? MutableApiConfigsManager) { + "MutableApiConfigsManager isn't available in build type ${BuildConfig.BUILD_TYPE}." + } + + init { + subscribeOnApiConfigs() + } + + /** Setup navigation state property by router [router] */ + fun setupNavigation(router: InnerTesterRouter) { + _uiState.update { + it.copy(onBackClick = router::back, onApplyChangesClick = router::back) + } + } + + private fun subscribeOnApiConfigs() { + mutableApiConfigsManager.configs + .onEach { configs -> + _uiState.update { + it.copy(apiInfoList = configs.toUiModel()) + } + } + .launchIn(viewModelScope) + } + + private fun List.toUiModel(): ImmutableSet { + return map { config -> + EnvironmentTogglesScreenUM.ApiInfoUM( + name = config.id.name, + select = config.currentEnvironment.name, + url = config.environments[config.currentEnvironment] + ?: error("Current environment's url isn't found"), + environments = config.environments + .map { environment -> environment.key.name } + .toImmutableSet(), + ) + } + .toImmutableSet() + } + + private fun getInitialState(): EnvironmentTogglesScreenUM { + return EnvironmentTogglesScreenUM( + title = R.string.environment_toggles, + apiInfoList = persistentSetOf(), + onEnvironmentSelect = ::onToggleValueChange, + onBackClick = {}, + onApplyChangesClick = {}, + ) + } + + private fun onToggleValueChange(id: String, name: String) { + viewModelScope.launch { + mutableApiConfigsManager.changeEnvironment(id = id, environment = ApiEnvironment.valueOf(name)) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt index 736e01b6c6..c1e4e290bb 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt @@ -1,6 +1,7 @@ package com.tangem.feature.tester.presentation.featuretoggles.state import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle +import kotlinx.collections.immutable.ImmutableList /** * Content state of feature toggles screen @@ -11,7 +12,7 @@ import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatur * @property onApplyChangesClick the lambda to be invoked when apply changes button is pressed */ internal data class FeatureTogglesContentState( - val featureToggles: List, + val featureToggles: ImmutableList, val onToggleValueChange: (String, Boolean) -> Unit, val onBackClick: () -> Unit, val onApplyChangesClick: () -> Unit, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt index b43d13d2f3..0ebd8cadbf 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt @@ -3,11 +3,7 @@ package com.tangem.feature.tester.presentation.featuretoggles.ui import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Switch @@ -26,6 +22,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState +import kotlinx.collections.immutable.persistentListOf /** * Screen with feature toggles list @@ -56,7 +53,8 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) { PrimaryButton( text = stringResource(id = R.string.apply_changes), onClick = state.onApplyChangesClick, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .padding(TangemTheme.dimens.spacing16), ) } @@ -98,7 +96,7 @@ private fun PreviewFeatureTogglesScreen() { TangemThemePreview { FeatureTogglesScreen( state = FeatureTogglesContentState( - featureToggles = listOf( + featureToggles = persistentListOf( TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true), TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false), ), diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt index 2d810e1ebf..6200dd283a 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt @@ -13,6 +13,8 @@ import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureToggle import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import javax.inject.Inject @@ -64,9 +66,10 @@ internal class FeatureTogglesViewModel @Inject constructor( } } - private fun MutableFeatureTogglesManager.getTesterFeatureToggles(): List { + private fun MutableFeatureTogglesManager.getTesterFeatureToggles(): ImmutableList { return this .getFeatureToggles() .map { TesterFeatureToggle(it.key, it.value) } + .toImmutableList() } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt index a570b21660..a074e734b9 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt @@ -3,12 +3,14 @@ package com.tangem.feature.tester.presentation.menu.state /** * Content state of tester menu screen * - * @property onBackClick the lambda to be invoked when back button is pressed - * @property onFeatureTogglesClick the lambda to be invoked when feature toggles button is pressed - * @property onTesterActionsClick the lambda to be invoked when tester actions button is pressed + * @property onBackClick the lambda to be invoked when back button is pressed + * @property onFeatureTogglesClick the lambda to be invoked when feature toggles button is pressed + * @property onEnvironmentTogglesClick the lambda to be invoked when environment toggles button is pressed + * @property onTesterActionsClick the lambda to be invoked when tester actions button is pressed */ data class TesterMenuContentState( val onBackClick: () -> Unit, val onFeatureTogglesClick: () -> Unit, + val onEnvironmentTogglesClick: () -> Unit, val onTesterActionsClick: () -> Unit, ) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt index 39d9c5b95c..105ea332fd 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt @@ -1,12 +1,9 @@ package com.tangem.feature.tester.presentation.menu.ui import android.content.res.Configuration +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable @@ -27,6 +24,8 @@ import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState */ @Composable internal fun TesterMenuScreen(state: TesterMenuContentState) { + BackHandler(onBack = state.onBackClick) + Column( modifier = Modifier .fillMaxSize() @@ -36,6 +35,7 @@ internal fun TesterMenuScreen(state: TesterMenuContentState) { onBackClick = state.onBackClick, text = stringResource(id = R.string.tester_menu), ) + Column( modifier = Modifier .padding( @@ -50,12 +50,13 @@ internal fun TesterMenuScreen(state: TesterMenuContentState) { onClick = state.onFeatureTogglesClick, modifier = Modifier.fillMaxWidth(), ) + PrimaryButton( - text = stringResource(R.string.stand_toggles), - onClick = state.onFeatureTogglesClick, + text = stringResource(R.string.environment_toggles), + onClick = state.onEnvironmentTogglesClick, modifier = Modifier.fillMaxWidth(), - enabled = false, ) + PrimaryButton( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.tester_actions), @@ -74,6 +75,7 @@ private fun PreviewTesterMenuScreen() { state = TesterMenuContentState( onBackClick = {}, onFeatureTogglesClick = {}, + onEnvironmentTogglesClick = {}, onTesterActionsClick = {}, ), ) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index 903f287179..2d7622855b 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -6,5 +6,5 @@ package com.tangem.feature.tester.presentation.navigation [REDACTED_AUTHOR] */ internal enum class TesterScreen { - MENU, FEATURE_TOGGLES, TESTER_ACTIONS + MENU, FEATURE_TOGGLES, ENVIRONMENTS_TOGGLES, TESTER_ACTIONS } \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index ffc3114361..dfd6f4d39c 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ Tester menu Feature toggles Apply changes - Stand toggles + Environment toggles Tester actions Hide all currencies Toggle app theme - %s From 3d70a5c18054921c935e43139e8ec24bba23316a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 Aug 2024 20:11:59 +0300 Subject: [PATCH 44/87] 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_cyber_22.xml | 17 ++++++++++++ .../ui/src/main/res/drawable/img_cyber_22.xml | 26 +++++++++++++++++++ .../QuotesUnsupportedCurrenciesIdAdapter.kt | 2 ++ .../utils/ResponseCryptoCurrenciesFactory.kt | 1 + gradle/dependencies.toml | 2 +- .../tangem/blockchainsdk/utils/Blockchain.kt | 8 ++++++ 8 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 core/ui/src/main/res/drawable/ic_cyber_22.xml create mode 100644 core/ui/src/main/res/drawable/img_cyber_22.xml diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index 51a162df5c..409a6b6067 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -698,6 +698,16 @@ "networkId": "blast/test" } ] + }, + { + "id": "cyberconnect", + "name": "Cyber", + "symbol": "ETH", + "networks": [ + { + "networkId": "cyber/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 a1613fcb79..f59561c295 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 @@ -72,6 +72,7 @@ fun getActiveIconRes(blockchainId: String): Int { "bittensor" -> R.drawable.img_bittensor_22 "blast", "blast/test" -> R.drawable.img_blast_22 "filecoin" -> R.drawable.img_filecoin_22 + "cyber", "cyber/test" -> R.drawable.img_cyber_22 else -> R.drawable.ic_alert_24 } } @@ -145,6 +146,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "bittensor" -> R.drawable.img_bittensor_22 "blast", "blast/test" -> R.drawable.img_blast_22 "filecoin" -> R.drawable.img_filecoin_22 + "cyber", "cyber/test" -> R.drawable.img_cyber_22 else -> R.drawable.ic_alert_24 } } @@ -215,6 +217,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "bittensor" -> R.drawable.img_bittensor_22 "blast", "blast/test" -> R.drawable.img_blast_22 "filecoin" -> R.drawable.img_filecoin_22 + "cyber", "cyber/test" -> R.drawable.img_cyber_22 else -> R.drawable.ic_alert_24 } } @@ -288,6 +291,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "bittensor" -> R.drawable.ic_bittensor_22 "blast", "blast/test" -> R.drawable.ic_blast_22 "filecoin" -> R.drawable.ic_filecoin_22 + "cyber", "cyber/test" -> R.drawable.ic_cyber_22 else -> R.drawable.ic_alert_24 } } @@ -361,6 +365,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "bittensor" -> R.drawable.ic_bittensor_22 "blast", "blast/test" -> R.drawable.ic_blast_22 "filecoin" -> R.drawable.ic_filecoin_22 + "cyber", "cyber/test" -> R.drawable.ic_cyber_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_cyber_22.xml b/core/ui/src/main/res/drawable/ic_cyber_22.xml new file mode 100644 index 0000000000..b622330ee9 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_cyber_22.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_cyber_22.xml b/core/ui/src/main/res/drawable/img_cyber_22.xml new file mode 100644 index 0000000000..e6af2f1807 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_cyber_22.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + 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 819dbe2a39..2a3ff567e6 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 @@ -56,6 +56,8 @@ internal class QuotesUnsupportedCurrenciesIdAdapter { "polygon-zkevm-ethereum" to "ethereum", "aurora-ethereum" to "ethereum", "base-ethereum" to "ethereum", + "cyberconnect" to "ethereum", + "blast" to "ethereum", ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt index 97c5d2ed4d..909d818b47 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt @@ -105,6 +105,7 @@ class ResponseCryptoCurrenciesFactory { Blockchain.BaseTestnet, Blockchain.Telos, Blockchain.Cronos, + Blockchain.Cyber, -> this.fullName else -> responseToken.name } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 740b7c1570..beb5d52f7e 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-728" +tangemBlockchainSdk = "develop-729" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 63f530a1d7..c9be6557a7 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -121,6 +121,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "filecoin" -> Blockchain.Filecoin "blast" -> Blockchain.Blast "blast/test" -> Blockchain.BlastTestnet + "cyber" -> Blockchain.Cyber + "cyber/test" -> Blockchain.CyberTestnet else -> null } } @@ -243,9 +245,14 @@ fun Blockchain.toNetworkId(): String { Blockchain.Filecoin -> "filecoin" Blockchain.Blast -> "blast" Blockchain.BlastTestnet -> "blast/test" + Blockchain.Cyber -> "cyber" + Blockchain.CyberTestnet -> "cyber/test" } } +/** + * CoinId is id from tangem backend response coin "id" field + */ @Suppress("ComplexMethod", "LongMethod") fun Blockchain.toCoinId(): String { return when (this) { @@ -321,6 +328,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Bittensor -> "bittensor" Blockchain.Filecoin -> "filecoin" Blockchain.Blast, Blockchain.BlastTestnet -> "blast" + Blockchain.Cyber, Blockchain.CyberTestnet -> "cyberconnect" } } From 54800d32ccbbb610cf1e12edbb33ee2dd447c768 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 Aug 2024 11:14:25 +0500 Subject: [PATCH 45/87] Updated on 2026-08-14 --- .../java/com/tangem/tap/TangemApplication.kt | 7 ---- .../tap/common/redux/global/GlobalAction.kt | 2 - .../tap/common/redux/global/GlobalReducer.kt | 1 - .../tap/common/redux/global/GlobalState.kt | 2 - .../warningMessage/WarningMessagesManager.kt | 42 ------------------- 5 files changed, 54 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 4e62a55ab3..0c1d250993 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.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.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tasks.product.DerivationsFinder import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles @@ -228,8 +227,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { ) } - initWarningMessagesManager() - loadNativeLibraries() if (LogConfig.network.blockchainSdkNetwork) { @@ -377,8 +374,4 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { ) store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager)) } - - private fun initWarningMessagesManager() { - store.dispatch(GlobalAction.SetWarningManager(WarningMessagesManager())) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 614b69566d..2b9e953b46 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -12,7 +12,6 @@ import com.tangem.tap.common.redux.DebugErrorAction import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import org.rekotlin.Action sealed class GlobalAction : Action { @@ -71,7 +70,6 @@ sealed class GlobalAction : Action { ) : GlobalAction() data class SetConfigManager(val configManager: ConfigManager) : GlobalAction() - data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction() data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction() data class SendEmail(val feedbackData: FeedbackData, val scanResponse: ScanResponse?) : GlobalAction() diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 043426c2de..6b011082d2 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -53,7 +53,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde is GlobalAction.SetConfigManager -> { globalState.copy(configManager = action.configManager) } - is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager) is GlobalAction.UpdateWalletSignedHashes -> { val card = globalState.scanResponse?.card ?: return globalState val wallet = card.wallets diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 50f8f85f47..420951087b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -7,7 +7,6 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.domain.TapWalletManager -import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import org.rekotlin.StateType @@ -19,7 +18,6 @@ data class GlobalState( val cardVerifiedOnline: Boolean = false, val tapWalletManager: TapWalletManager = TapWalletManager(), val configManager: ConfigManager? = null, - val warningManager: WarningMessagesManager? = null, val feedbackManager: LegacyFeedbackManager? = null, val appCurrency: AppCurrency = AppCurrency.Default, val scanCardFailsCounter: Int = 0, diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt deleted file mode 100644 index caa4125ff7..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.tangem.tap.domain.configurable.warningMessage - -import com.tangem.blockchain.common.Blockchain -import java.util.concurrent.CopyOnWriteArrayList - -/** -[REDACTED_AUTHOR] - */ -// TODO: Delete with SendFeatureToggles -@Deprecated(message = "Used only in old send screen") -class WarningMessagesManager { - - private val warningsList = CopyOnWriteArrayList() - - fun getWarnings(location: WarningMessage.Location, blockchains: List): List { - return warningsList.filter { message -> - val messageBlockchains = message.blockchainList - val isCorrespondingMessageBlockchains = messageBlockchains == null || - messageBlockchains.any(blockchains::contains) - val isCorrespondingMessageLocation = message.location.contains(location) - - !message.isHidden && isCorrespondingMessageLocation && isCorrespondingMessageBlockchains - } - } - - fun hideWarning(warning: WarningMessage): Boolean { - val foundWarning = findWarning(warning) ?: return false - val isCorrectType = foundWarning.type == WarningMessage.Type.Temporary || - foundWarning.type == WarningMessage.Type.AppRating - - return if (!foundWarning.isHidden && isCorrectType) { - foundWarning.isHidden = true - true - } else { - false - } - } - - private fun findWarning(warning: WarningMessage): WarningMessage? { - return warningsList.firstOrNull { it == warning } - } -} \ No newline at end of file From faa58cf142d422f7af5568c465745d120cb2f5c1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 Aug 2024 14:38:46 +0300 Subject: [PATCH 46/87] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 7 + .../tangem/common/ui/charts/MarketChart.kt | 2 +- core/res/src/main/res/values-de/strings.xml | 29 +- core/res/src/main/res/values-fr/strings.xml | 6 +- core/res/src/main/res/values-it/strings.xml | 1 - core/res/src/main/res/values-ja/strings.xml | 12 +- core/res/src/main/res/values-ru/strings.xml | 19 +- .../src/main/res/values-uk-rUA/strings.xml | 10 +- .../src/main/res/values-zh-rTW/strings.xml | 7 - core/res/src/main/res/values/strings.xml | 7 +- .../marketprice/PriceChangeInPercent.kt | 10 +- .../core/ui/utils/BigDecimalFormatter.kt | 43 +++ .../core/ui/utils/DateTimeFormatters.kt | 23 +- .../com/tangem/core/ui/utils/DateUtils.kt | 7 + .../markets/DefaultMarketsTokenRepository.kt | 20 +- .../data/markets/MarketsBatchUpdateFetcher.kt | 10 +- .../markets/converters/TokenChartConverter.kt | 2 +- .../converters/TokenListConfigConverters.kt | 6 +- .../converters/TokenMarketChartsConverter.kt | 10 +- .../converters/TokenMarketInfoConverter.kt | 25 +- .../converters/TokenMarketListConverter.kt | 15 +- ...verter.kt => TokenQuotesShortConverter.kt} | 17 +- .../com/tangem/domain/markets/TokenMarket.kt | 2 +- .../tangem/domain/markets/TokenMarketInfo.kt | 13 +- .../com/tangem/domain/markets/TokenQuotes.kt | 20 +- .../tangem/domain/markets/TokenQuotesShort.kt | 10 + .../domain/markets/GetTokenQuotesUseCase.kt | 19 ++ .../repositories/MarketsTokenRepository.kt | 2 + .../details/api/TokenMarketSerializable.kt | 8 +- .../DefaultMarketsTokenDetailsComponent.kt | 20 +- .../impl/model/MarketsTokenDetailsModel.kt | 255 ++++++++++++------ .../converters/PricePerformanceConverter.kt | 5 +- .../impl/model/formatter/Formatters.kt | 98 +++++++ .../formatter/MarketsDateTimeFormatters.kt | 130 +++++++++ .../impl/ui/MarketsTokenDetailsContent.kt | 61 ++++- .../ui/components/PricePerformanceBlock.kt | 1 - .../impl/ui/state/MarketsTokenDetailsUM.kt | 3 + .../converters/MarketsTokenItemConverter.kt | 28 +- .../presentation/state/StakingNotification.kt | 4 +- 39 files changed, 722 insertions(+), 245 deletions(-) rename data/markets/src/main/java/com/tangem/data/markets/converters/{TokenQuotesConverter.kt => TokenQuotesShortConverter.kt} (50%) create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotesShort.kt create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index e96df425a1..28b30cb9e3 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.markets.GetTokenMarketInfoUseCase import com.tangem.domain.markets.GetTokenPriceChartUseCase +import com.tangem.domain.markets.GetTokenQuotesUseCase import com.tangem.domain.markets.repositories.MarketsTokenRepository import dagger.Module import dagger.Provides @@ -33,4 +34,10 @@ object MarketsDomainModule { fun provideGetTokenMarketInfoUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenMarketInfoUseCase { return GetTokenMarketInfoUseCase(marketsTokenRepository = marketsTokenRepository) } + + @Provides + @Singleton + fun provideGetTokenQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenQuotesUseCase { + return GetTokenQuotesUseCase(marketsTokenRepository = marketsTokenRepository) + } } \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt index 917b2eab45..2e850fd87d 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -295,7 +295,7 @@ private fun MarketChartPreview( updateLook { it.copy( xAxisFormatter = { value -> - value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd) + value.toLong().toTimeFormat(DateTimeFormatters.dateMMMdd) }, yAxisFormatter = { value -> value.setScale(3, RoundingMode.HALF_UP).toPlainString() diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 6d99e6bd52..51b2bd8358 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,7 +1,7 @@ Netzwerk wählen - Benutzerdef. Token hinzuf. + Token anlegen Token verwalten Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen. So scannt man @@ -88,7 +88,7 @@ Kopieren Adresse kopieren Erstellen - Benutzerdef. + Eigen %d tag %d tage @@ -126,6 +126,7 @@ Passphrase Einfügen %1$s-%2$s + %1$s — %2$s Weiterlesen Empfangen Ablehnen @@ -278,7 +279,7 @@ Rückmeldung Feedback zu Tangem Eine Transaktion kann nicht gesendet werden - Aktuelle Transaktion + Nur diese Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. Gib das Genehmigungslimit für das ausgewählte Token an Betrag %s @@ -341,7 +342,7 @@ Verfügbare Netzwerke Mein Portfolio Markt - Um Adressen für ausgewählte Netzwerke zu generieren, musst du eine Tangem-Karte scannen/ einsetzen + Um Adressen für ausgewählte Netzwerke zu generieren, musst du eine Tangem-Karte scannen Die Daten konnten nicht geladen werden... Schnelle Aktionen Ergebnis @@ -446,7 +447,7 @@ Möchtest du den Aktivierungsprozess abbrechen? Erste Schritte Für die Karte, die du hinzufügen möchtest, wurde bereits eine andere Wallets erstellt. Wenn du Guthaben auf dieser Wallets hast, hebe es bitte ab, setze diese Karte zurück und füge sie als Backup hinzu. - Erstellen eines Backups + Backups anlegen Lese mehr über die Seed-Phrase leer @@ -499,7 +500,7 @@ Gruppe erstellen Nach Guthaben Token organisieren - Gruppierung aufh. + Gruppe löschen Wählen aus der Galerie aus Einstellungen Du hast keinen Zugriff auf deine Kamera gewährt @@ -647,7 +648,7 @@ Aktiv Um deine Kryptos zu unstaken, klick hier. Die Anzahl der zu stakenden Krypros muss mindesten %s betragen - nicht gestakte beanspruche + Nicht gestakte beanspruche Jährliche prozentuale Rendite Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst. Effektiver Jahreszins @@ -671,7 +672,10 @@ Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking. Migrieren Natives Staking - Mit Staking kannst du %1$s verdienen. Deine Staking-Belohnungen kommen alle ~%2$s Tage. + Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden tag. + Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Stunde. + Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden Monat. + Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche. Verdiene Staking-Belohnungen Die Belohnungen werden sofort nach dem unstaken gestoppt. Der unstakingprozess dauert %s. Erneut binden @@ -679,10 +683,11 @@ Belohnungen erneut staken Widerrufen Neuwahl - automatisch - händisch + Automatisch + Händisch Block Tag + Täglich Epoche Ära Stunde @@ -693,7 +698,7 @@ Mehr staken Stake %s Staking beenden%s - gelocktes unlocken + Gelocktes unlocken Unstaken Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen Staking beenden @@ -834,7 +839,7 @@ Aktivierungsfehler Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen. BNB Beacon Chain wird abgeschaltet - ausbaufähig + Ausbaufähig Gefällt mir OK, habe ich verstanden! Echt toll! diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 3c55b9ec8c..9155dc35a7 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -123,6 +123,7 @@ Passphrase Coller %1$s-%2$s + %1$s — %2$s En savoir plus Recevoir Rejeter @@ -269,7 +270,7 @@ Commentaires Commentaires sur Tangem Impossible d\'envoyer une transaction - Transaction en cours + Transaction Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange. Spécifiez la limite approuvée pour le jeton sélectionné Montant %s @@ -328,7 +329,7 @@ Ajouter au portfolio Mon portfolio Marché - Pour générer des adresses pour les réseaux sélectionnés, vous devez scanner votre carte Tangem. + Pour générer des adresses pour les réseaux sélectionnés, vous devez scanner votre carte Tangem Sélectionnez un portefeuille Trier par Idées @@ -594,7 +595,6 @@ Période d\'échauffement Le temps imparti pour activer la participation au staking. Native staking - Le staking vous permet d\'en gagner %1$s. Vos récompenses de staking arrivent tous les ~%2$s jours. Gagnez des récompenses de staking Récompenses Staker plus diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index b8860bd7d8..e7caa7829e 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -14,7 +14,6 @@ Mantieni le modifiche Invia Con successo - Avviso Codice di accesso Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto Mantenimento della carta diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index d5d7502641..6d695bc564 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -124,6 +124,7 @@ パスフレーズ ペースト %1$s-%2$s + %1$s — %2$s 続きを読む 受け取る 拒否 @@ -276,7 +277,7 @@ フィードバック Tangemへのフィードバック 取引を送信できません - 現在の取引 + 取引 ネットワークは、あなたがトークンのスワップを承認していることを確認するために、トークン承認手数料を請求します。 選択したトークンの承認制限を指定します 金額%s @@ -636,7 +637,7 @@ 資産のステーキングを解除するには、ここをクリックしてください。 ステーキング金額は %s 以上である必要があります ステーキング解除分を請求する - APY + 年率 ステーキングに参加することで得られる年間収益率。 APR 利用可能 @@ -659,7 +660,10 @@ ステーキングへの参加を有効にするために割り当てられた時間。 移行 ネイティブステーキング - ステーキングにより%1$sを獲得できます。ステーキング報酬は ~ %2$s日ごとに届きます。 + ステーキングにより%1$sを獲得できます。ステーキング報酬は毎日受け取れます。 + ステーキングにより%1$sを獲得できます。ステーキング報酬は1時間ごとに受け取れます。 + ステーキングにより%1$sを獲得できます。ステーキング報酬は毎月受け取れます。 + ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。 ステーキング報酬を獲得 ステーキング解除後、報酬の獲得はすぐに停止します。ステーキング解除プロセスには%sかかります。 再結束 @@ -671,6 +675,7 @@ 手動 ブロック + 毎日 エポック 時代 @@ -686,6 +691,7 @@ 資産を請求するために、unstakedを確認してください ステーキング解除 バリデーター + バリデーター 投票する 投票はロックされています 引き出す diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index f7c6ecf61c..9df99375b9 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -47,7 +47,7 @@ Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте. Использовать эту карту для сброса кода доступа на других картах в этом кошельке - Восстановление кода доступа + Восстановление доступа Сбросить Вы уверены, что хотите это сделать? Смена кода доступа @@ -128,6 +128,7 @@ Парольная фраза Вставить %1$s-%2$s + %1$s — %2$s Подробнее Получить Отклонить @@ -628,7 +629,6 @@ Для завершения стейкинга нажмите сюда Сумма для стейкинга должна быть не менее %s Забрать средства - APY Годовой процентный доход, который вы можете получить от участия в стейкинге. APR Доступно @@ -650,13 +650,26 @@ Время, необходимое для начала процесса стейкинга и активации процесса начисления наград Переместить Нативный стейкинг - Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый %2$s + Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый день. + Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый час. + Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый месяц. + Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждую неделю. Получите награду за стейкинг Награда перестанет начисляться сразу после завершения стейкинга. Процесс завершения длится %s. Повторный стейкинг Застейкать вознаграждения Отозвать Переголосовать + Автоматически + Вручную + Блок + День + Каждый день + Эпоха + Эра + Час + Месяц + Неделя Вознаграждения Застейкать еще Застейкать %s diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index bdb802eb7e..d3c560257c 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -130,6 +130,7 @@ Парольна фраза Вставити %1$s-%2$s + %1$s — %2$s Детальніше Отримати Відхилити @@ -282,7 +283,7 @@ Звернення в підтримку Звернення в підтримку Tangem Не вдається відправити транзакцію - Поточна транзакція + Транзакція Мережа стягує комісію за схвалення токену за підтвердження, що саме ви дозволяєте використовувати ваш токен для обміну. Вкажіть ліміт доступу для обраного токена Кількість %s @@ -347,7 +348,7 @@ Доступні мережі Моє портфоліо Маркет - Щоб згенерувати адреси для обраних мереж, потрібно відсканувати свою картку Tangem. + Щоб згенерувати адреси для обраних мереж, потрібно відсканувати свою картку Tangem Не вдалося завантажити дані... Швидкі дії Результат @@ -682,7 +683,10 @@ Відведений час для активації участі в стейкінгу. Перемістити Нативний стейкінг - Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг надходять кожні ~%2$s днів. + Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щодня. + Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються кожну годину. + Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щомісяця. + Стейкінг дозволяє заробляти %1$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 acd0b259b8..eb5bc2de81 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -9,9 +9,6 @@ 原因:%s 無法發送交易 此卡不支持%1$s網路上的代幣因為韌體限制 - 感謝您的反饋。我們會盡快回复 - 你的建議已送出 - 請嘗試完全按照動畫中顯示的方式點擊卡片或請求支持 有困難在掃描卡上嗎? 此卡不適用於此app 轉到設置以在 Tangem App 中啟用生物識別身份驗證 @@ -56,7 +53,6 @@ 創造 刪除 禁用 - 斷開連接 完成 允許 啟用 @@ -69,7 +65,6 @@ 主卡片 拒絕 重新命名 - 重試 保存設置 搜索 搜尋代幣 @@ -88,7 +83,6 @@ 交易 我了解 無法觸達 - 警告 已複製代幣地址 支持的網路 @@ -121,7 +115,6 @@ App Currency 發行人 簽署 - 如果您忘記密碼,您將無法使用您的資金。無法恢復代碼 更多 檢查您的網路連接或切換到其他網絡 服務條款 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 5a4b8c08bb..23d562fd61 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -126,6 +126,7 @@ Passphrase Paste %1$s-%2$s + %1$s — %2$s Read more Receive Reject @@ -667,7 +668,10 @@ The allocated time for activating participation in staking. Migrate Native staking - Staking allow you to earn %1$s. Your staking rewards arrive every ~%2$s days. + Staking allow you to earn %1$s. Your staking rewards arrive every day. + Staking allow you to earn %1$s. Your staking rewards arrive every hour. + Staking allow you to earn %1$s. Your staking rewards arrive every month. + Staking allow you to earn %1$s. Your staking rewards arrive every week. Earn staking rewards Rewards stop accruing immediately after you unstake. The unstaking process takes %s. Rebond @@ -679,6 +683,7 @@ Manual Block Day + Each day Epoch Era Hour diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt index d2c55ed576..a202d8bf21 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt @@ -1,10 +1,7 @@ package com.tangem.core.ui.components.marketprice import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -26,6 +23,11 @@ fun PriceChangeInPercent( modifier: Modifier = Modifier, textStyle: TextStyle = TangemTheme.typography.body2, ) { + if (valueInPercent.isBlank()) { + Box(modifier) + return + } + Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, 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 40e51a085c..d1d4cf9c8c 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 @@ -25,6 +25,13 @@ object BigDecimalFormatter { private const val FIAT_MARKET_DEFAULT_DIGITS = 2 private const val FIAT_MARKET_EXTENDED_DIGITS = 6 + private val bigDecimal01 = BigDecimal("0.1") + private val bigDecimal001 = BigDecimal("0.01") + private val bigDecimal0001 = BigDecimal("0.001") + private val bigDecimal00001 = BigDecimal("0.0001") + private val bigDecimal000001 = BigDecimal("0.00001") + private val bigDecimal0000001 = BigDecimal("0.000001") + fun formatCryptoAmount( cryptoAmount: BigDecimal?, cryptoCurrency: String, @@ -169,6 +176,28 @@ object BigDecimalFormatter { .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) } + fun formatFiatPriceUncapped( + fiatAmount: BigDecimal?, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), + ): String { + if (fiatAmount == null) return EMPTY_BALANCE_SIGN + val formatterCurrency = getCurrency(fiatCurrencyCode) + + val decimals = getProperFiatPriceDecimals(fiatAmount) + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = decimals + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + roundingMode = RoundingMode.HALF_UP + } + + return formatter.format(fiatAmount) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + } + fun formatFiatEditableAmount( fiatAmount: String?, fiatCurrencyCode: String, @@ -330,4 +359,18 @@ object BigDecimalFormatter { } private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD + + @Suppress("MagicNumber") + fun getProperFiatPriceDecimals(price: BigDecimal): Int { + return when { + price >= BigDecimal.ONE -> 2 + price >= bigDecimal01 -> 3 + price >= bigDecimal001 -> 4 + price >= bigDecimal0001 -> 6 + price >= bigDecimal00001 -> 8 + price >= bigDecimal000001 -> 10 + price >= bigDecimal0000001 -> 12 + else -> price.stripTrailingZeros().scale() + } + } } \ 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 7c6fa15b74..20296aec51 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 @@ -10,8 +10,6 @@ import java.util.Locale @Suppress("MagicNumber") object DateTimeFormatters { - private const val DDMMYYYY = "dd.MM.yyyy" - /** * Two SS means, SHORT style for date and time. * If pattern contains "a", it means time is in 12 hour format. @@ -51,7 +49,7 @@ object DateTimeFormatters { val dateDDMMYYYY: DateTimeFormatter by lazy { DateTimeFormatterBuilder() - .appendPattern(DDMMYYYY) + .appendPattern("dd.MM.yyyy") .toFormatter() .withLocale(Locale.getDefault()) } @@ -59,18 +57,12 @@ object DateTimeFormatters { /** * In API version < 24, there may be some problems with getting the best date and time format pattern. */ - val dateMMMMd: DateTimeFormatter by lazy { - DateTimeFormatterBuilder() - .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "dd MMM")) - .toFormatter() - .withLocale(Locale.getDefault()) + val dateMMMdd: DateTimeFormatter by lazy { + getBestFormatterBySkeleton("dd MMM") } val dateYYYY: DateTimeFormatter by lazy { - DateTimeFormatterBuilder() - .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyy")) - .toFormatter() - .withLocale(Locale.getDefault()) + getBestFormatterBySkeleton("yyyy") } val dateTimeFormatter: DateTimeFormatter by lazy { @@ -80,4 +72,11 @@ object DateTimeFormatters { fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String { return formatter.print(date) } + + fun getBestFormatterBySkeleton(skeleton: String): DateTimeFormatter { + return DateTimeFormatterBuilder() + .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton)) + .toFormatter() + .withLocale(Locale.getDefault()) + } } \ 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 bc2560f6e9..e01b50dfb3 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,12 @@ fun Long.toDateFormatWithTodayYesterday(formatter: DateTimeFormatter = DateTimeF * Returns formatted time according to [formatter]. */ fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeFormatter): String { + return formatAsDateTime(formatter) +} + +/** + * Returns formatted date-time according to [formatter]. + */ +fun Long.formatAsDateTime(formatter: DateTimeFormatter): String { return DateTimeFormatters.formatDate(date = DateTime(this, DateTimeZone.getDefault()), formatter = formatter) } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index e670fce633..e850d1ed35 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -19,9 +19,6 @@ internal class DefaultMarketsTokenRepository( private val dispatcherProvider: CoroutineDispatcherProvider, ) : MarketsTokenRepository { - private val tokenListConverter = TokenMarketListConverter() - private val tokenChartConverter = TokenChartConverter() - private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher( prefetchDistance = firstBatchSize, batchSize = nextBatchSize, @@ -65,7 +62,7 @@ internal class DefaultMarketsTokenRepository( val last = res.tokens.size < request.limit return BatchFetchResult.Success( - data = tokenListConverter.convert(res), + data = TokenMarketListConverter.convert(res), last = last, empty = res.tokens.isEmpty(), ) @@ -105,7 +102,7 @@ internal class DefaultMarketsTokenRepository( interval = interval.toRequestParam(), ) - return tokenChartConverter.convert(interval, response.getOrThrow()) + return TokenChartConverter.convert(interval, response.getOrThrow()) } override suspend fun getTokenInfo( @@ -119,6 +116,17 @@ internal class DefaultMarketsTokenRepository( language = languageCode, ) - return TokenMarketInfoConverter().convert(response.getOrThrow()) + return TokenMarketInfoConverter.convert(response.getOrThrow()) + } + + override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes { + // TODO change method when backend is ready + val response = marketsApi.getCoinMarketData( + currency = fiatCurrencyCode, + coinId = tokenId, + language = "en", + ) + + return TokenMarketInfoConverter.convert(response.getOrThrow()).quotes } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt index 0da0fcd457..d1db77b24e 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt @@ -1,8 +1,7 @@ package com.tangem.data.markets -import com.tangem.data.markets.converters.TokenChartConverter import com.tangem.data.markets.converters.TokenMarketChartsConverter -import com.tangem.data.markets.converters.TokenQuotesConverter +import com.tangem.data.markets.converters.TokenQuotesShortConverter import com.tangem.data.markets.converters.toRequestParam import com.tangem.data.markets.utils.retryOnError import com.tangem.datasource.api.common.response.getOrThrow @@ -21,9 +20,6 @@ internal class MarketsBatchUpdateFetcher( private val tangemTechApi: TangemTechApi, ) : BatchUpdateFetcher, TokenMarketUpdateRequest> { - private val tokenListChartsConverter = TokenMarketChartsConverter(TokenChartConverter()) - private val tokenQuotesConverter = TokenQuotesConverter() - override suspend fun BatchUpdateFetcher.UpdateContext>.fetchUpdateAsync( toUpdate: List>>, updateRequest: TokenMarketUpdateRequest, @@ -75,7 +71,7 @@ internal class MarketsBatchUpdateFetcher( val res = toUpdate.map { batch -> batch.copy( data = batch.data.map { - it.copy(tokenQuotes = tokenQuotesConverter.convert(it.id, quotesRes)) + it.copy(tokenQuotesShort = TokenQuotesShortConverter.convert(it.id, quotesRes)) }, ) } @@ -97,7 +93,7 @@ internal class MarketsBatchUpdateFetcher( key = batchToUpdate.key, data = batchToUpdate.data.map { it.copy( - tokenCharts = tokenListChartsConverter.convert( + tokenCharts = TokenMarketChartsConverter.convert( chartsToCopy = it.tokenCharts, tokenId = it.id, interval = updateRequest.interval, diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenChartConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenChartConverter.kt index bdd90f2733..744e6fef48 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenChartConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenChartConverter.kt @@ -4,7 +4,7 @@ import com.tangem.datasource.api.markets.models.response.TokenMarketChartRespons import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenChart -class TokenChartConverter { +internal object TokenChartConverter { fun convert(interval: PriceChangeInterval, value: TokenMarketChartResponse): TokenChart { return TokenChart( diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt index eee63ef4e9..1875a5b609 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt @@ -3,13 +3,13 @@ package com.tangem.data.markets.converters import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarketListConfig -fun TokenMarketListConfig.Interval.toRequestParam(): String = when (this) { +internal fun TokenMarketListConfig.Interval.toRequestParam(): String = when (this) { TokenMarketListConfig.Interval.H24 -> "24h" TokenMarketListConfig.Interval.WEEK -> "1w" TokenMarketListConfig.Interval.MONTH -> "30d" } -fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) { +internal fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) { TokenMarketListConfig.Order.ByRating -> "rating" TokenMarketListConfig.Order.Trending -> "trending" TokenMarketListConfig.Order.Buyers -> "buyers" @@ -17,7 +17,7 @@ fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) { TokenMarketListConfig.Order.TopLosers -> "losers" } -fun PriceChangeInterval.toRequestParam(): String = when (this) { +internal fun PriceChangeInterval.toRequestParam(): String = when (this) { PriceChangeInterval.H24 -> "24h" PriceChangeInterval.WEEK -> "1w" PriceChangeInterval.MONTH -> "1m" diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt index 88eccb1163..fdd44ffba9 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt @@ -5,9 +5,7 @@ import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarket import com.tangem.domain.markets.TokenMarketListConfig -class TokenMarketChartsConverter( - private val tokenChartConverter: TokenChartConverter, -) { +internal object TokenMarketChartsConverter { fun convert( chartsToCopy: TokenMarket.Charts, @@ -20,13 +18,13 @@ class TokenMarketChartsConverter( } return when (interval) { TokenMarketListConfig.Interval.H24 -> chartsToCopy.copy( - h24 = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices), + h24 = TokenChartConverter.convert(interval.toPriceChangeInterval(), prices), ) TokenMarketListConfig.Interval.WEEK -> chartsToCopy.copy( - week = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices), + week = TokenChartConverter.convert(interval.toPriceChangeInterval(), prices), ) TokenMarketListConfig.Interval.MONTH -> chartsToCopy.copy( - month = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices), + month = TokenChartConverter.convert(interval.toPriceChangeInterval(), prices), ) else -> error("unsupported interval=$interval. This shouldn't have happened.") } diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt index 8693393e68..e9314b0d7e 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -2,9 +2,10 @@ package com.tangem.data.markets.converters import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenQuotes import com.tangem.utils.converter.Converter -internal class TokenMarketInfoConverter : Converter { +internal object TokenMarketInfoConverter : Converter { override fun convert(value: TokenMarketInfoResponse): TokenMarketInfo { return with(value) { @@ -12,8 +13,7 @@ internal class TokenMarketInfoConverter : Converter> { +internal object TokenMarketListConverter : Converter> { override fun convert(value: TokenMarketListResponse): List { val imageHost = value.imageHost ?: run { @@ -25,13 +24,11 @@ class TokenMarketListConverter : Converter?, val shortDescription: String?, val fullDescription: String?, @@ -16,16 +15,6 @@ data class TokenMarketInfo( val links: Links?, val pricePerformance: PricePerformance?, ) { - data class PriceChangePercentage( - val day: BigDecimal?, - val week: BigDecimal?, - val month: BigDecimal?, - val threeMonths: BigDecimal?, - val sixMonths: BigDecimal?, - val year: BigDecimal?, - val allTime: BigDecimal?, - ) - data class Network( val networkId: String, val exchangeable: Boolean, diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt index 7a162c9aac..18826801e3 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt @@ -4,15 +4,11 @@ import java.math.BigDecimal data class TokenQuotes( val currentPrice: BigDecimal, - private val priceChanges: Map, -) { - init { - require(priceChanges.containsKey(PriceChangeInterval.H24)) - require(priceChanges.containsKey(PriceChangeInterval.WEEK)) - require(priceChanges.containsKey(PriceChangeInterval.MONTH)) - } - - fun h24Percent() = priceChanges[PriceChangeInterval.H24]!! - fun weekPercent() = priceChanges[PriceChangeInterval.WEEK]!! - fun monthPercent() = priceChanges[PriceChangeInterval.MONTH]!! -} \ No newline at end of file + val h24ChangePercent: BigDecimal?, + val weekChangePercent: BigDecimal?, + val monthChangePercent: BigDecimal?, + val m3ChangePercent: BigDecimal?, + val m6ChangePercent: BigDecimal?, + val yearChangePercent: BigDecimal?, + val allTimeChangePercent: BigDecimal?, +) \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotesShort.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotesShort.kt new file mode 100644 index 0000000000..3b6bb3bdf1 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotesShort.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenQuotesShort( + val currentPrice: BigDecimal, + val h24ChangePercent: BigDecimal, + val weekChangePercent: BigDecimal, + val monthChangePercent: BigDecimal, +) \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt new file mode 100644 index 0000000000..6477010d57 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenQuotesUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.markets + +import arrow.core.Either +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.repositories.MarketsTokenRepository + +class GetTokenQuotesUseCase( + private val marketsTokenRepository: MarketsTokenRepository, +) { + + suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either { + return Either.catch { + marketsTokenRepository.getTokenQuotes( + fiatCurrencyCode = appCurrency.code, + tokenId = tokenId, + ) + }.mapLeft {} + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index e6e3a23e03..5c80144aec 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -13,4 +13,6 @@ interface MarketsTokenRepository { suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String, languageCode: String): TokenMarketInfo + + suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/TokenMarketSerializable.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/TokenMarketSerializable.kt index 4e978ed5ee..a1aa6b178c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/TokenMarketSerializable.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/api/TokenMarketSerializable.kt @@ -30,10 +30,10 @@ fun TokenMarket.toSerializable(): TokenMarketSerializable { symbol = symbol, marketCap = marketCap, tokenQuotes = TokenMarketSerializable.Quotes( - currentPrice = tokenQuotes.currentPrice, - h24Percent = tokenQuotes.h24Percent(), - weekPercent = tokenQuotes.weekPercent(), - monthPercent = tokenQuotes.monthPercent(), + currentPrice = tokenQuotesShort.currentPrice, + h24Percent = tokenQuotesShort.h24ChangePercent, + weekPercent = tokenQuotesShort.weekChangePercent, + monthPercent = tokenQuotesShort.monthChangePercent, ), imageUrl = imageUrlLarge, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt index 5bc189be06..402356487a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -1,11 +1,9 @@ package com.tangem.features.markets.details.impl -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp +import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel @@ -32,11 +30,23 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier, ) { + LifecycleStartEffect(Unit) { + model.isVisibleOnScreen.value = true + onStopOrDispose { + model.isVisibleOnScreen.value = false + } + } + val state by model.state.collectAsStateWithLifecycle() + val bsState by bottomSheetState + + LaunchedEffect(bsState) { + model.containerBottomSheetState.value = bsState + } MarketsTokenDetailsContent( state = state, - onBackClick = { onBack() }, + onBackClick = onBack, onHeaderSizeChange = onHeaderSizeChange, modifier = modifier, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 2359614915..9f9d524a84 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -9,19 +9,22 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.marketprice.PriceChangeType +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.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.core.ui.utils.DateTimeFormatters -import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.GetTokenMarketInfoUseCase -import com.tangem.domain.markets.GetTokenPriceChartUseCase -import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.* +import com.tangem.features.markets.component.BottomSheetState import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter +import com.tangem.features.markets.details.impl.model.formatter.* +import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice +import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween +import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM import com.tangem.features.markets.impl.R @@ -30,13 +33,15 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import org.joda.time.DateTime import java.math.BigDecimal -import java.math.RoundingMode import javax.inject.Inject -@Suppress("LargeClass") +@Suppress("LargeClass", "LongParameterList") @Stable internal class MarketsTokenDetailsModel @Inject constructor( paramsContainer: ParamsContainer, @@ -44,10 +49,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, + private val getTokenQuotesUseCase: GetTokenQuotesUseCase, private val urlOpener: UrlOpener, ) : Model() { - val params = paramsContainer.require() + private var quotesJob = JobHolder() + private val params = paramsContainer.require() private val currentAppCurrency = getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> @@ -77,13 +84,13 @@ internal class MarketsTokenDetailsModel @Inject constructor( chartData = MarketChartData.NoData.Loading updateLook { + val percentChangeType = params.token.tokenQuotes.h24Percent.percentChangeType() + it.copy( - type = getChartTypeByPercent(params.token.tokenQuotes.h24Percent), - xAxisFormatter = { value -> - value.toLong().toTimeFormat(DateTimeFormatters.timeFormatter) - }, + type = percentChangeType.toChartType(), + xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24), yAxisFormatter = { value -> - BigDecimalFormatter.formatFiatAmountUncapped( + BigDecimalFormatter.formatFiatPriceUncapped( fiatAmount = value, fiatCurrencyCode = currentAppCurrency.value.code, fiatCurrencySymbol = "", @@ -93,10 +100,28 @@ internal class MarketsTokenDetailsModel @Inject constructor( } } + private val currentQuotes = MutableStateFlow( + TokenQuotes( + currentPrice = params.token.tokenQuotes.currentPrice, + h24ChangePercent = params.token.tokenQuotes.h24Percent, + weekChangePercent = params.token.tokenQuotes.weekPercent, + monthChangePercent = params.token.tokenQuotes.monthPercent, + m3ChangePercent = null, + m6ChangePercent = null, + yearChangePercent = null, + allTimeChangePercent = null, + ), + ) + + private var lastUpdatedTimestamp: Long = DateTime.now().millis + + val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED) + val isVisibleOnScreen = MutableStateFlow(false) + val state = MutableStateFlow( MarketsTokenDetailsUM( tokenName = params.token.name, - priceText = BigDecimalFormatter.formatFiatAmountUncapped( + priceText = BigDecimalFormatter.formatFiatPriceUncapped( fiatAmount = params.token.tokenQuotes.currentPrice, fiatCurrencyCode = currentAppCurrency.value.code, fiatCurrencySymbol = currentAppCurrency.value.symbol, @@ -121,7 +146,9 @@ internal class MarketsTokenDetailsModel @Inject constructor( ), selectedInterval = PriceChangeInterval.H24, onSelectedIntervalChange = ::onSelectedIntervalChange, + markerSet = false, body = MarketsTokenDetailsUM.Body.Loading, + triggerPriceChange = consumedEvent(), infoBottomSheet = TangemBottomSheetConfig( isShow = false, onDismissRequest = {}, @@ -146,21 +173,22 @@ internal class MarketsTokenDetailsModel @Inject constructor( } private fun initialLoad() { - loadChart(state.value.selectedInterval) loadInfo() + loadChart(state.value.selectedInterval) + modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS) } - private fun onSelectedIntervalChange(interval: PriceChangeInterval) { - if (state.value.selectedInterval == interval) return - - state.update { - it.copy( - selectedInterval = interval, - priceChangeType = PriceChangeType.UP, + private fun loadQuotes() { + modelScope.launch { + val result = getTokenQuotesUseCase( + tokenId = params.token.id, + appCurrency = currentAppCurrency.value, ) - } - loadChart(interval) + result.onRight { res -> + updateQuotes(res) + } + } } private fun loadChart(interval: PriceChangeInterval) { @@ -192,7 +220,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } - val xAxisFormatter = getFormatterByInterval(state.value.selectedInterval) + val xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(state.value.selectedInterval) chart.onRight { chartDataProducer.runTransactionSuspend { @@ -204,6 +232,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( updateLook { it.copy( xAxisFormatter = xAxisFormatter, + type = state.value.priceChangeType.toChartType(), ) } } @@ -247,8 +276,15 @@ internal class MarketsTokenDetailsModel @Inject constructor( tokenMarketInfo.fold( ifRight = { result -> + currentQuotes.value = result.quotes + val percent = result.quotes.getPercentByInterval(interval = state.value.selectedInterval) state.update { it.copy( + priceText = result.quotes.currentPrice.formatAsPrice(currentAppCurrency.value), + priceChangePercentText = result.quotes.getFormattedPercentByInterval( + interval = it.selectedInterval, + ), + priceChangeType = percent.percentChangeType(), body = MarketsTokenDetailsUM.Body.Content( description = descriptionConverter.convert(result), infoBlocks = infoConverter.convert(result), @@ -275,57 +311,101 @@ internal class MarketsTokenDetailsModel @Inject constructor( } } - private fun getFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { - return when (interval) { - PriceChangeInterval.H24 -> { value: BigDecimal -> - value.toLong().toTimeFormat(DateTimeFormatters.timeFormatter) - } - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - PriceChangeInterval.MONTH6, - -> { value -> - value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd) - } - PriceChangeInterval.YEAR -> { value -> - value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd) - } - PriceChangeInterval.ALL_TIME -> { value -> - value.toLong().toTimeFormat(DateTimeFormatters.dateYYYY) - } + private suspend fun updateQuotes(newQuotes: TokenQuotes) { + val triggerPriceChangeType = getFormattedPriceChange( + currentPrice = currentQuotes.value.currentPrice, + updatedPrice = newQuotes.currentPrice, + ) + val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) { + triggeredEvent( + data = triggerPriceChangeType, + onConsume = { + state.update { it.copy(triggerPriceChange = consumedEvent()) } + }, + ) + } else { + consumedEvent() + } + + val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval) + val priceChangeType = percent.percentChangeType() + + // wait until marker is removed + state.first { it.markerSet.not() } + + currentQuotes.value = newQuotes + lastUpdatedTimestamp = DateTime.now().millis + + state.update { stateToUpdate -> + stateToUpdate.copy( + priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency.value), + priceChangePercentText = newQuotes.getFormattedPercentByInterval( + interval = stateToUpdate.selectedInterval, + ), + priceChangeType = priceChangeType, + triggerPriceChange = trigger, + dateTimeText = getDefaultDateTimeString(stateToUpdate.selectedInterval), + ) + } + } + + private fun onSelectedIntervalChange(interval: PriceChangeInterval) { + if (state.value.selectedInterval == interval) return + + val quotes = currentQuotes.value + val priceChangePercent = quotes.getFormattedPercentByInterval(interval) + + state.update { + it.copy( + priceChangePercentText = priceChangePercent, + selectedInterval = interval, + priceChangeType = quotes.getPercentByInterval(interval)?.percentChangeType() + ?: PriceChangeType.NEUTRAL, + dateTimeText = getDefaultDateTimeString(interval), + ) + } + + loadChart(interval) + + if (priceChangePercent.isEmpty()) { + loadQuotes() } } @Suppress("MagicNumber") - private fun onMarkerPointSelected(time: BigDecimal?, price: BigDecimal?) { - val timeText = time?.toLong()?.toTimeFormat(DateTimeFormatters.dateTimeFormatter)?.let { - resourceReference(R.string.common_range, wrappedList(it, resourceReference(R.string.common_now))) - } ?: resourceReference(R.string.common_today) + private fun onMarkerPointSelected(markerTimestamp: BigDecimal?, price: BigDecimal?) { + val currentState = state.value - val percent = price?.subtract(params.token.tokenQuotes.currentPrice) - ?.divide(params.token.tokenQuotes.currentPrice, 4, RoundingMode.HALF_UP) - ?.multiply(BigDecimal(-100)) - ?: params.token.tokenQuotes.h24Percent + val dateTimeText = markerTimestamp?.let { + MarketsDateTimeFormatters.formatDateByIntervalWithMarker( + interval = currentState.selectedInterval, + markerTimestamp = it, + ) + } ?: getDefaultDateTimeString(currentState.selectedInterval) - val percentText = BigDecimalFormatter.formatPercent( - percent = percent, - useAbsoluteValue = true, - ) + val priceText = (price ?: currentQuotes.value.currentPrice).formatAsPrice(currentAppCurrency.value) - state.update { - it.copy( - dateTimeText = timeText, - priceText = BigDecimalFormatter.formatFiatAmountUncapped( - fiatAmount = price ?: params.token.tokenQuotes.currentPrice, - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ), + val percent = price?.let { + getChangePercentBetween( + previousPrice = it, + currentPrice = currentQuotes.value.currentPrice, + ) + } ?: currentQuotes.value.getPercentByInterval(currentState.selectedInterval) + + val percentText = percent?.let { + BigDecimalFormatter.formatPercent( + percent = it, + useAbsoluteValue = true, + ) + } ?: "" + + state.update { stateToUpdate -> + stateToUpdate.copy( + markerSet = markerTimestamp != null, + dateTimeText = dateTimeText, + priceText = priceText, priceChangePercentText = percentText, - priceChangeType = when { - percent < BigDecimal.ZERO -> PriceChangeType.DOWN - percent > BigDecimal.ZERO -> PriceChangeType.UP - else -> PriceChangeType.NEUTRAL - }, + priceChangeType = percent.percentChangeType(), ) } @@ -338,14 +418,6 @@ internal class MarketsTokenDetailsModel @Inject constructor( } } - private fun getChartTypeByPercent(percent: BigDecimal): MarketChartLook.Type { - return if (percent >= BigDecimal.ZERO) { - MarketChartLook.Type.Growing - } else { - MarketChartLook.Type.Falling - } - } - private fun showInfoBottomSheet(content: InfoBottomSheetContent) { state.update { stateToUpdate -> stateToUpdate.copy( @@ -379,6 +451,35 @@ internal class MarketsTokenDetailsModel @Inject constructor( currentState.body is MarketsTokenDetailsUM.Body.Nothing ) { loadInfo() + modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS) } } + + private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) { + launch { + while (true) { + delay(timeMillis) + // Update quotes only when the container bottom sheet is in the expanded state + containerBottomSheetState.first { it == BottomSheetState.EXPANDED } + // and is visible on the screen + isVisibleOnScreen.first { it } + + loadQuotes() + } + }.saveIn(quotesJob) + } + + private fun getDefaultDateTimeString(interval: PriceChangeInterval): TextReference { + return MarketsDateTimeFormatters.formatDateByInterval( + interval = interval, + startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval( + interval = interval, + currentTimestamp = lastUpdatedTimestamp, + ), + ) + } + + private companion object { + const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt index b08b518ba0..0ccf01cde7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt @@ -42,8 +42,9 @@ internal class PricePerformanceConverter( private fun BigDecimal?.convert(): String { val currency = appCurrency() - return BigDecimalFormatter.formatCompactFiatAmount( - amount = this, + + return BigDecimalFormatter.formatFiatPriceUncapped( + fiatAmount = this, fiatCurrencyCode = currency.code, fiatCurrencySymbol = currency.symbol, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt new file mode 100644 index 0000000000..fd560e41d7 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt @@ -0,0 +1,98 @@ +package com.tangem.features.markets.details.impl.model.formatter + +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenQuotes +import java.math.BigDecimal +import java.math.RoundingMode + +internal fun BigDecimal.formatAsPrice(currency: AppCurrency): String { + return BigDecimalFormatter.formatFiatPriceUncapped( + fiatAmount = this, + fiatCurrencyCode = currency.code, + fiatCurrencySymbol = currency.symbol, + ) +} + +internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInterval): String { + val percent = when (interval) { + PriceChangeInterval.H24 -> h24ChangePercent + PriceChangeInterval.WEEK -> weekChangePercent + PriceChangeInterval.MONTH -> monthChangePercent + PriceChangeInterval.MONTH3 -> m3ChangePercent + PriceChangeInterval.MONTH6 -> m6ChangePercent + PriceChangeInterval.YEAR -> yearChangePercent + PriceChangeInterval.ALL_TIME -> allTimeChangePercent + } + + return percent?.let { + BigDecimalFormatter.formatPercent( + percent = it, + useAbsoluteValue = true, + ) + } ?: "" +} + +internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): BigDecimal? { + return when (interval) { + PriceChangeInterval.H24 -> h24ChangePercent + PriceChangeInterval.WEEK -> weekChangePercent + PriceChangeInterval.MONTH -> monthChangePercent + PriceChangeInterval.MONTH3 -> m3ChangePercent + PriceChangeInterval.MONTH6 -> m6ChangePercent + PriceChangeInterval.YEAR -> yearChangePercent + PriceChangeInterval.ALL_TIME -> allTimeChangePercent + } +} + +internal fun BigDecimal?.percentChangeType(): PriceChangeType { + return when { + this == null -> PriceChangeType.NEUTRAL + this > BigDecimal.ZERO -> PriceChangeType.UP + this < BigDecimal.ZERO -> PriceChangeType.DOWN + else -> PriceChangeType.NEUTRAL + } +} + +@Suppress("MagicNumber") +internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: BigDecimal): BigDecimal { + return if (previousPrice == BigDecimal.ZERO) { + BigDecimal.ZERO + } else { + currentPrice.subtract(previousPrice).divide(previousPrice, 4, RoundingMode.HALF_UP) + } +} + +internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType { + val updatedPriceDecimals = BigDecimalFormatter.getProperFiatPriceDecimals(updatedPrice) + + val current = currentPrice.setScale(updatedPriceDecimals, RoundingMode.HALF_UP) + val updated = updatedPrice.setScale(updatedPriceDecimals, RoundingMode.HALF_UP) + + return when { + updated > current -> PriceChangeType.UP + updated < current -> PriceChangeType.DOWN + else -> PriceChangeType.NEUTRAL + } +} + +internal fun PriceChangeType.toChartType(): MarketChartLook.Type { + return when (this) { + PriceChangeType.UP -> MarketChartLook.Type.Growing + PriceChangeType.DOWN -> MarketChartLook.Type.Falling + PriceChangeType.NEUTRAL -> MarketChartLook.Type.Growing + } +} + +internal fun getChartTypeByPercent(percent: BigDecimal?): MarketChartLook.Type { + return when { + percent == null -> return MarketChartLook.Type.Growing + percent == BigDecimal.ZERO -> MarketChartLook.Type.Growing + percent > BigDecimal.ZERO -> MarketChartLook.Type.Growing + percent < BigDecimal.ZERO -> MarketChartLook.Type.Falling + else -> MarketChartLook.Type.Growing + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt new file mode 100644 index 0000000000..e93456eb34 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt @@ -0,0 +1,130 @@ +package com.tangem.features.markets.details.impl.model.formatter + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.formatAsDateTime +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.markets.impl.R +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import java.math.BigDecimal + +internal object MarketsDateTimeFormatters { + + private const val H24_MILLIS = 24L * 60 * 60 * 1000 + private const val WEEK_MILLIS = 7L * H24_MILLIS + + private val dateTimeMMMFormatter by lazy { + DateTimeFormatters.getBestFormatterBySkeleton("dd MMM hh:mm") + } + + private val dateFormatter = DateTimeFormatters.dateDDMMYYYY + + internal fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { + return when (interval) { + PriceChangeInterval.H24 -> { value: BigDecimal -> + value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter) + } + PriceChangeInterval.WEEK, + PriceChangeInterval.MONTH, + PriceChangeInterval.MONTH3, + PriceChangeInterval.MONTH6, + -> { value -> + value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd) + } + PriceChangeInterval.YEAR -> { value -> + value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd) + } + PriceChangeInterval.ALL_TIME -> { value -> + value.toLong().formatAsDateTime(DateTimeFormatters.dateYYYY) + } + } + } + + internal fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference { + return when (interval) { + PriceChangeInterval.H24 -> resourceReference(R.string.common_today) + PriceChangeInterval.WEEK, + PriceChangeInterval.MONTH, + PriceChangeInterval.MONTH3, + -> { + resourceReference( + R.string.common_range_with_space, + wrappedList( + stringReference( + startTimestamp.formatAsDateTime(MarketsDateTimeFormatters.dateTimeMMMFormatter), + ), + resourceReference(R.string.common_now), + ), + ) + } + PriceChangeInterval.MONTH6, + PriceChangeInterval.YEAR, + -> { + resourceReference( + R.string.common_range_with_space, + wrappedList( + stringReference( + startTimestamp.formatAsDateTime(MarketsDateTimeFormatters.dateFormatter), + ), + resourceReference(R.string.common_now), + ), + ) + } + PriceChangeInterval.ALL_TIME -> resourceReference(R.string.common_all) + } + } + + internal fun formatDateByIntervalWithMarker( + interval: PriceChangeInterval, + markerTimestamp: BigDecimal, + ): TextReference { + return when (interval) { + PriceChangeInterval.H24, + PriceChangeInterval.WEEK, + PriceChangeInterval.MONTH, + PriceChangeInterval.MONTH3, + -> { + resourceReference( + R.string.common_range_with_space, + wrappedList( + stringReference( + markerTimestamp.toLong().formatAsDateTime(MarketsDateTimeFormatters.dateTimeMMMFormatter), + ), + resourceReference(R.string.common_now), + ), + ) + } + PriceChangeInterval.MONTH6, + PriceChangeInterval.YEAR, + PriceChangeInterval.ALL_TIME, + -> { + resourceReference( + R.string.common_range_with_space, + wrappedList( + stringReference( + markerTimestamp.toLong().formatAsDateTime(MarketsDateTimeFormatters.dateFormatter), + ), + resourceReference(R.string.common_now), + ), + ) + } + } + } + + @Suppress("MagicNumber") + fun getStartTimestampByInterval(interval: PriceChangeInterval, currentTimestamp: Long): Long { + return when (interval) { + PriceChangeInterval.H24 -> currentTimestamp - H24_MILLIS + PriceChangeInterval.WEEK -> currentTimestamp - WEEK_MILLIS + PriceChangeInterval.MONTH -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(1).millis + PriceChangeInterval.MONTH3 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(3).millis + PriceChangeInterval.MONTH6 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(6).millis + PriceChangeInterval.YEAR -> DateTime(currentTimestamp, DateTimeZone.UTC).minusYears(1).millis + PriceChangeInterval.ALL_TIME -> 0 + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index 7f8dfd5def..ecfc715abd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -1,10 +1,13 @@ package com.tangem.features.markets.details.impl.ui +import androidx.compose.animation.Animatable +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -23,6 +26,9 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.marketprice.PriceChangeInPercent import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference @@ -134,11 +140,12 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween, ) { - Column { - Text( - text = state.priceText, - style = TangemTheme.typography.head, - color = TangemTheme.colors.text.primary1, + Column( + Modifier.weight(1f), + ) { + TokenPriceText( + price = state.priceText, + triggerPriceChange = state.triggerPriceChange, ) Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { Text( @@ -155,7 +162,7 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) } SpacerW4() CoinIcon( - modifier = Modifier.size(TangemTheme.dimens.size48), + modifier = Modifier.requiredSize(TangemTheme.dimens.size48), url = state.iconUrl, alpha = 1f, colorFilter = null, @@ -164,6 +171,38 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) } } +@Composable +private fun TokenPriceText( + price: String, + triggerPriceChange: StateEvent, + modifier: Modifier = Modifier, +) { + val growColor = TangemTheme.colors.text.accent + val fallColor = TangemTheme.colors.text.warning + val generalColor = TangemTheme.colors.text.primary1 + + val color = remember { Animatable(generalColor) } + + EventEffect(triggerPriceChange) { + val nextColor = when (it) { + PriceChangeType.UP, + -> growColor + PriceChangeType.DOWN -> fallColor + PriceChangeType.NEUTRAL -> return@EventEffect + } + + color.animateTo(nextColor, snap()) + color.animateTo(generalColor, tween(durationMillis = 500)) + } + + Text( + modifier = modifier, + text = price, + color = color.value, + style = TangemTheme.typography.head, + ) +} + @Composable private fun IntervalSelector( trendInterval: PriceChangeInterval, @@ -224,9 +263,9 @@ private fun Preview() { modifier = Modifier.background(TangemTheme.colors.background.tertiary), state = MarketsTokenDetailsUM( tokenName = "Token Name", - priceText = "Price", - dateTimeText = stringReference("Date Time"), - priceChangePercentText = "Price Change", + priceText = "$0.00000000324", + dateTimeText = stringReference("Today"), + priceChangePercentText = "52.00%", iconUrl = "", priceChangeType = PriceChangeType.UP, chartState = MarketsTokenDetailsUM.ChartState( @@ -244,6 +283,8 @@ private fun Preview() { onDismissRequest = {}, content = TangemBottomSheetConfigContent.Empty, ), + markerSet = false, + triggerPriceChange = consumedEvent(), ), onHeaderSizeChange = {}, onBackClick = {}, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt index cb3098ca60..66a09fb533 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -1,7 +1,6 @@ package com.tangem.features.markets.details.impl.ui.components import android.content.res.Configuration -import androidx.compose.animation.* import androidx.compose.foundation.layout.* import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Text diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt index 42f7c3e9e0..473bc3a518 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt @@ -5,6 +5,7 @@ import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.markets.PriceChangeInterval import java.math.BigDecimal @@ -17,9 +18,11 @@ internal data class MarketsTokenDetailsUM( val priceChangePercentText: String, val priceChangeType: PriceChangeType, val selectedInterval: PriceChangeInterval, + val markerSet: Boolean, val chartState: ChartState, val onSelectedIntervalChange: (PriceChangeInterval) -> Unit, val infoBottomSheet: TangemBottomSheetConfig, + val triggerPriceChange: StateEvent, val body: Body, ) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index 590aae7f3f..c43e29f406 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -48,17 +48,17 @@ internal class MarketsTokenItemConverter( ratingPosition = new.marketRating?.toString(), marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() }, iconUrl = new.imageUrlLarge, - price = ifChanged(prev = prev.tokenQuotes, new = new.tokenQuotes, prevR = prevUI.price) { + price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) { new.getCurrentPrice( prev = prev, ) }, trendPercentText = ifChanged( - prev.tokenQuotes, - new.tokenQuotes, + prev.tokenQuotesShort, + new.tokenQuotesShort, prevUI.trendPercentText, ) { new.getTrendPercent() }, - trendType = ifChanged(prev.tokenQuotes, new.tokenQuotes, prevUI.trendType) { new.getTrendType() }, + trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() }, chardData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chardData) { new.getChartData() }, ) } @@ -79,16 +79,16 @@ internal class MarketsTokenItemConverter( } private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { - val prevPrice = prev?.tokenQuotes?.currentPrice + val prevPrice = prev?.tokenQuotesShort?.currentPrice - val priceText = BigDecimalFormatter.formatFiatAmountUncapped( - fiatAmount = tokenQuotes.currentPrice, + val priceText = BigDecimalFormatter.formatFiatPriceUncapped( + fiatAmount = tokenQuotesShort.currentPrice, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, ) val changeType = if (prevPrice != null) { - if (tokenQuotes.currentPrice > prevPrice) { + if (tokenQuotesShort.currentPrice > prevPrice) { PriceChangeType.UP } else { PriceChangeType.DOWN @@ -122,9 +122,9 @@ internal class MarketsTokenItemConverter( private fun TokenMarket.getTrendType(): PriceChangeType { val percent = when (currentTrendInterval) { - TrendInterval.H24 -> tokenQuotes.h24Percent() - TrendInterval.D7 -> tokenQuotes.weekPercent() - TrendInterval.M1 -> tokenQuotes.monthPercent() + TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent + TrendInterval.D7 -> tokenQuotesShort.weekChangePercent + TrendInterval.M1 -> tokenQuotesShort.monthChangePercent }.setScale(2, RoundingMode.UP) return when (percent.compareTo(BigDecimal.ZERO)) { @@ -136,9 +136,9 @@ internal class MarketsTokenItemConverter( private fun TokenMarket.getTrendPercent(): String { val percent = when (currentTrendInterval) { - TrendInterval.H24 -> tokenQuotes.h24Percent() - TrendInterval.D7 -> tokenQuotes.weekPercent() - TrendInterval.M1 -> tokenQuotes.monthPercent() + TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent + TrendInterval.D7 -> tokenQuotesShort.weekChangePercent + TrendInterval.M1 -> tokenQuotesShort.monthChangePercent } return BigDecimalFormatter.formatPercent( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt index 525eed8a61..327f1e521c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -4,7 +4,6 @@ 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.stringReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.staking.impl.R internal sealed class StakingNotification(val config: NotificationConfig) { @@ -55,8 +54,7 @@ internal sealed class StakingNotification(val config: NotificationConfig) { ) : Warning( title = resourceReference(R.string.staking_notification_earn_rewards_title), subtitle = resourceReference( - R.string.staking_notification_earn_rewards_text, - wrappedList(currencyName, days), + R.string.common_error, // FIXME ), ) } From ea8c9079b7101752c51d71df8b632569b3f7a2b1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 Aug 2024 13:54:35 +0500 Subject: [PATCH 47/87] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 12 +++ .../converters/AmountStateConverter.kt | 2 +- .../GiveTxPermisssionBottomSheet.kt | 11 ++- .../permission/state/GiveTxPermissionState.kt | 2 +- core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-fr/strings.xml | 2 +- core/res/src/main/res/values-ja/strings.xml | 2 +- .../src/main/res/values-uk-rUA/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 2 +- .../data/staking/DefaultStakingRepository.kt | 53 +++++++------ .../domain/staking/IsApproveNeededUseCase.kt | 18 +++++ .../staking/repositories/StakingRepository.kt | 6 +- .../repository/MockStakingRepository.kt | 8 +- .../impl/presentation/state/StakingUiState.kt | 1 + .../ConfirmationStatePreviewData.kt | 1 + .../state/stub/StakingClickIntentsStub.kt | 4 + .../SetButtonsStateTransformer.kt | 69 +++++++++------- .../SetConfirmationStateAssentTransformer.kt | 4 +- .../SetConfirmationStateLoadingTransformer.kt | 3 +- ...pprovalBottomSheetInProgressTransformer.kt | 34 ++++++++ .../ShowApprovalBottomSheetTransformer.kt | 78 +++++++++++++++++++ .../impl/presentation/ui/StakingScreen.kt | 3 + .../viewmodel/StakingClickIntents.kt | 4 + .../viewmodel/StakingViewModel.kt | 57 +++++++++++++- 24 files changed, 311 insertions(+), 69 deletions(-) create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetInProgressTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index c280208083..b4f55832a9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -156,4 +156,16 @@ internal object StakingDomainModule { stakingErrorResolver = stakingErrorResolver, ) } + + @Provides + @Singleton + fun provideIsApproveNeededUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): IsApproveNeededUseCase { + return IsApproveNeededUseCase( + stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index ad251378f6..410a23b468 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -54,7 +54,7 @@ class AmountStateConverter( return AmountState.Data( walletName = userWallet.name, - walletBalance = resourceReference(R.string.send_wallet_balance_format, wrappedList(crypto, fiat)), + walletBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), tokenIconState = iconStateConverter.convert(status), amountTextField = amountFieldConverter.convert(value), isPrimaryButtonEnabled = false, diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index ba65ad6d9c..6f61b650b4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -93,10 +93,12 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh PrimaryButtonIconEnd( text = stringResource(id = R.string.common_approve), iconResId = R.drawable.ic_tangem_24, + showProgress = data.approveButton.loading, modifier = Modifier .fillMaxWidth() .padding(horizontal = TangemTheme.dimens.spacing16), onClick = data.approveButton.onClick, + enabled = data.approveButton.enabled, ) SpacerH12() @@ -107,6 +109,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh .fillMaxWidth() .padding(horizontal = TangemTheme.dimens.spacing16), onClick = content.onCancel, + enabled = data.cancelButton.enabled, ) SpacerH16() @@ -151,7 +154,7 @@ private fun AmountItem( currency: String, approveType: ApproveType, approveItems: ImmutableList, - onChangeApproveType: (ApproveType) -> Unit, + onChangeApproveType: ((ApproveType) -> Unit)?, ) { var isExpandSelector by remember { mutableStateOf(false) } var amountSize by remember { mutableStateOf(IntSize.Zero) } @@ -208,8 +211,10 @@ private fun AmountItem( isExpanded = isExpandSelector, onDismiss = { isExpandSelector = false }, onItemClick = { approveType -> - isExpandSelector = false - onChangeApproveType.invoke(approveType) + onChangeApproveType?.let { + isExpandSelector = false + onChangeApproveType.invoke(approveType) + } }, items = approveItems, selectedType = approveType, diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt index dbdce22ba9..908e42559b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt @@ -22,7 +22,7 @@ sealed class GiveTxPermissionState { val approveItems: ImmutableList = ApproveType.entries.toImmutableList(), val approveButton: ApprovePermissionButton, val cancelButton: CancelPermissionButton, - val onChangeApproveType: (ApproveType) -> Unit, + val onChangeApproveType: ((ApproveType) -> Unit)? = null, ) : GiveTxPermissionState() } diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 51b2bd8358..1e9ed93914 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -88,6 +88,7 @@ Kopieren Adresse kopieren Erstellen + %1$s (%2$s) Eigen %d tag @@ -639,7 +640,6 @@ Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert %1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. Ungültige Adresse - %1$s (%2$s) Transaktion gesendet Bereite das Scannen der Karte vor, die du einrichten möchtest. Entferne diese Wallet diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 9155dc35a7..43f28f5375 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -86,6 +86,7 @@ Copier Copier l\'adresse Créer + %1$s (%2$s) Personnalisé %d jour @@ -567,7 +568,6 @@ 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 %1$s est un actif du réseau Tron. Pour calculer les frais et effectuer une transaction, déposez du Tron (TRX) sur votre compte. Adresse incorrecte - %1$s (%2$s) Transaction envoyée Préparez-vous à numériser la carte que vous souhaitez configurer. Oublier le portefeuille diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 6d695bc564..56465b3e0b 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -87,6 +87,7 @@ コピー アドレスをコピー 作成 + %1$s ( %2$s ) 設定 %d 日 @@ -627,7 +628,6 @@ 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 無効なアドレス - %1$s ( %2$s ) 取引が送信されました セットアップしたいカードをスキャンするために準備してください。 ウォレット削除 diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index d3c560257c..d352a7be3c 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -90,6 +90,7 @@ Копіювати Скопіювати адресу Створити + %1$s (%2$s) Власна %d день @@ -651,7 +652,6 @@ Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron (TRX) на свій рахунок. Недійсна адреса - %1$s (%2$s) Трансакцію надіслано Підготуйтеся до сканування картку, яку потрібно налаштувати. Забути гаманець diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 23d562fd61..956b064d05 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -88,6 +88,7 @@ Copy Copy address Create + %1$s (%2$s) Custom %d day @@ -635,7 +636,6 @@ Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account. Invalid address - %1$s (%2$s) Transaction sent Prepare to scan card you want to setup. Forget wallet diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index c5748f49a8..8d89311bdd 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -52,7 +52,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -@Suppress("LargeClass", "LongParameterList") +@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, private val appPreferencesStore: AppPreferencesStore, @@ -99,8 +99,8 @@ internal class DefaultStakingRepository( value = emptyMap(), ) - override fun isStakingSupported(currencyId: String): Boolean { - return integrationIdMap.containsKey(currencyId) + override fun isStakingSupported(integrationKey: String): Boolean { + return integrationIdMap.containsKey(integrationKey) } override suspend fun fetchEnabledYields(refresh: Boolean) { @@ -153,7 +153,7 @@ internal class DefaultStakingRepository( val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, symbol) - val isSupported = isStakingSupported(rawCurrencyId) + val isSupported = isStakingSupported(cryptoCurrencyId.getIntegrationKey()) when { prefetchedYield != null && isSupported -> { @@ -250,10 +250,7 @@ internal class DefaultStakingRepository( if (!stakingFeatureToggle.isStakingEnabled) return@withContext val cryptoCurrency = address.cryptoCurrency - val rawCurrencyId = - cryptoCurrency.id.rawCurrencyId ?: error("Staking custom tokens is not available") - - val integrationId = integrationIdMap[rawCurrencyId] ?: return@withContext + val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: return@withContext cacheRegistry.invokeOnExpire( key = getYieldBalancesKey(userWalletId), @@ -284,7 +281,7 @@ internal class DefaultStakingRepository( send(YieldBalance.Empty) } else { launch(dispatchers.io) { - val integrationId = integrationIdMap[address.cryptoCurrency.id.rawCurrencyId] + val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()] ?: error("Could not get integrationId") stakingBalanceStore.get(integrationId) .collectLatest { @@ -317,7 +314,7 @@ internal class DefaultStakingRepository( } else { fetchSingleYieldBalance(userWalletId, address) - val integrationId = integrationIdMap[address.cryptoCurrency.id.rawCurrencyId] + val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()] ?: error("Could not get integrationId") val result = stakingBalanceStore.getSyncOrNull(integrationId) ?: return@withContext YieldBalance.Error yieldBalanceConverter.convert( @@ -347,8 +344,7 @@ internal class DefaultStakingRepository( addresses .mapNotNull { networkAddress -> val cryptoCurrency = networkAddress.cryptoCurrency - val rawCurrencyId = cryptoCurrency.id.rawCurrencyId ?: error("Currency raw id is null") - val integrationId = integrationIdMap[rawCurrencyId] + val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] if (integrationId != null) { networkAddress.address to integrationId @@ -534,6 +530,13 @@ internal class DefaultStakingRepository( } } + override fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean { + return when (cryptoCurrency.id.getIntegrationKey()) { + Blockchain.Ethereum.id.plus(Blockchain.Polygon.toCoinId()) -> true + else -> false + } + } + private fun findPrefetchedYield(yields: List, currencyId: String, symbol: String): Yield? { return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } } @@ -559,13 +562,15 @@ internal class DefaultStakingRepository( private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}" + private fun CryptoCurrency.ID.getIntegrationKey(): String = rawNetworkId.plus(rawCurrencyId) + private companion object { const val YIELDS_STORE_KEY = "yields" const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" + const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking" const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" - const val ETHEREUM_INTEGRATION_ID = "ethereum-matic-native-staking" const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" const val TRON_INTEGRATION_ID = "tron-trx-native-staking" const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" @@ -576,17 +581,17 @@ internal class DefaultStakingRepository( // uncomment items as implementation is ready val integrationIdMap = mapOf( - Blockchain.Solana.toCoinId() to SOLANA_INTEGRATION_ID, - Blockchain.Cosmos.toCoinId() to COSMOS_INTEGRATION_ID, - // Blockchain.Polkadot.toCoinId() to POLKADOT_INTEGRATION_ID, - // Blockchain.Polygon.toCoinId() to ETHEREUM_INTEGRATION_ID, - // Blockchain.Avalanche.toCoinId() to AVALANCHE_INTEGRATION_ID, - // Blockchain.Tron.toCoinId() to TRON_INTEGRATION_ID, - // Blockchain.Cronos.toCoinId() to CRONOS_INTEGRATION_ID, - // Blockchain.BSC.toCoinId() to BINANCE_INTEGRATION_ID, - // Blockchain.Kava.toCoinId() to KAVA_INTEGRATION_ID, - // Blockchain.Near.toCoinId() to NEAR_INTEGRATION_ID, - // Blockchain.Tezos.toCoinId() to TEZOS_INTEGRATION_ID, + Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID, + Blockchain.Cosmos.run { id + toCoinId() } to COSMOS_INTEGRATION_ID, + Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, + // Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID, + // Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID, + // Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, + // Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID, + // Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, + // Blockchain.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID, + // Blockchain.Near.run { id + toCoinId() } to NEAR_INTEGRATION_ID, + // Blockchain.Tezos.run { id + toCoinId() } to TEZOS_INTEGRATION_ID, ) } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt new file mode 100644 index 0000000000..d183b36a2f --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency + +class IsApproveNeededUseCase( + private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + operator fun invoke(cryptoCurrency: CryptoCurrency): Either { + return Either + .catch { stakingRepository.isApproveNeeded(cryptoCurrency) } + .mapLeft { stakingErrorResolver.resolve(it) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 5605958156..b80b7b95d2 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -15,9 +15,10 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +@Suppress("TooManyFunctions") interface StakingRepository { - fun isStakingSupported(currencyId: String): Boolean + fun isStakingSupported(integrationKey: String): Boolean suspend fun fetchEnabledYields(refresh: Boolean) @@ -75,4 +76,7 @@ interface StakingRepository { /** Returns whether additional staking is possible if there is already active staking */ fun isStakeMoreAvailable(networkId: Network.ID): Boolean + + /** Returns whether approve is needed before staking */ + fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 75e83715bb..b42fe2c3e4 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -2,11 +2,13 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lceFlow -import com.tangem.domain.staking.model.* +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata +import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.staking.model.stakekit.action.StakingActionType -import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.transaction.* import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency @@ -237,4 +239,6 @@ class MockStakingRepository : StakingRepository { } override fun isStakeMoreAvailable(networkId: Network.ID): Boolean = true + + override fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean = true } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 98b09f5e78..e9b4c44faa 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -88,6 +88,7 @@ internal sealed class StakingStates { val footerText: String, val transactionDoneState: TransactionDoneState, val pendingActionInProgress: PendingAction? = null, + val isApprovalNeeded: Boolean, ) : ConfirmationState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index 0994dd7cdb..4fc4fa0cde 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -86,5 +86,6 @@ internal object ConfirmationStatePreviewData { ), transactionDoneState = TransactionDoneState.Empty, pendingActions = persistentListOf(), + isApprovalNeeded = false, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 3036764d3d..c8e5a5d35a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -38,6 +38,10 @@ object StakingClickIntentsStub : StakingClickIntents { override fun openRewardsValidators() {} + override fun showApprovalBottomSheet() {} + + override fun onApprovalClick() {} + override fun onExploreClick() {} override fun onShareClick() {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 4332347169..c954aee97d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -135,23 +135,7 @@ internal class SetButtonsStateTransformer : Transformer { } } - StakingStep.Confirmation -> { - if (confirmationState is StakingStates.ConfirmationState.Data) { - if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { - resourceReference(R.string.common_close) - } else { - when (actionType) { - StakingActionCommonType.ENTER -> resourceReference(R.string.common_stake) - StakingActionCommonType.EXIT -> resourceReference(R.string.common_unstake) - StakingActionCommonType.PENDING_OTHER, - StakingActionCommonType.PENDING_REWARDS, - -> getPendingActionTitle(confirmationState.pendingActions.firstOrNull()?.type) - } - } - } else { - resourceReference(R.string.common_close) - } - } + StakingStep.Confirmation -> getConfirmationButtonText() StakingStep.Validators -> resourceReference(R.string.common_continue) StakingStep.Amount, StakingStep.RewardsValidators, @@ -159,6 +143,30 @@ internal class SetButtonsStateTransformer : Transformer { } } + private fun StakingUiState.getConfirmationButtonText(): TextReference { + return if (confirmationState is StakingStates.ConfirmationState.Data) { + if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { + resourceReference(R.string.common_close) + } else { + when (actionType) { + StakingActionCommonType.ENTER -> { + if (confirmationState.isApprovalNeeded) { + resourceReference(R.string.give_permission_title) + } else { + resourceReference(R.string.common_stake) + } + } + StakingActionCommonType.EXIT -> resourceReference(R.string.common_unstake) + StakingActionCommonType.PENDING_OTHER, + StakingActionCommonType.PENDING_REWARDS, + -> getPendingActionTitle(confirmationState.pendingActions.firstOrNull()?.type) + } + } + } else { + resourceReference(R.string.common_close) + } + } + private fun StakingUiState.onPrimaryClick() { when (currentStep) { StakingStep.InitialInfo -> { @@ -169,18 +177,27 @@ internal class SetButtonsStateTransformer : Transformer { StakingStep.Validators, StakingStep.Amount, -> clickIntents.onNextClick() - StakingStep.Confirmation -> { - if (confirmationState is StakingStates.ConfirmationState.Data) { - if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { - clickIntents.onBackClick() - } else { - clickIntents.onActionClick(confirmationState.pendingActions.firstOrNull()) - } + StakingStep.Confirmation -> onConfirmationClick() + StakingStep.RewardsValidators -> Unit + } + } + + private fun StakingUiState.onConfirmationClick() { + if (confirmationState is StakingStates.ConfirmationState.Data) { + if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { + clickIntents.onBackClick() + } else { + val isEnterAction = actionType == StakingActionCommonType.ENTER + val isApproveNeeded = confirmationState.isApprovalNeeded + + if (isEnterAction && isApproveNeeded) { + clickIntents.showApprovalBottomSheet() } else { - clickIntents.onBackClick() + clickIntents.onActionClick(confirmationState.pendingActions.firstOrNull()) } } - StakingStep.RewardsValidators -> Unit + } else { + clickIntents.onBackClick() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index f2dc22698e..33b475fcaf 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -6,8 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.staking.impl.presentation.state.FeeState -import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState +import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.Provider @@ -49,6 +48,7 @@ internal class SetConfirmationStateAssentTransformer( validatorState = validatorState.copySealed(isClickable = true), pendingActions = pendingActionList, isPrimaryButtonEnabled = true, + isApprovalNeeded = false, ) } else { return this diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 88662f7fde..04f8fd9de7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -20,7 +20,7 @@ internal class SetConfirmationStateLoadingTransformer( innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Loading, validatorState = ValidatorState.Content( - isClickable = false, + isClickable = true, chosenValidator = chosenValidator, availableValidators = yield.validators, ), @@ -34,6 +34,7 @@ internal class SetConfirmationStateLoadingTransformer( transactionDoneState = TransactionDoneState.Empty, pendingActions = persistentListOf(), pendingActionInProgress = null, + isApprovalNeeded = false, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetInProgressTransformer.kt new file mode 100644 index 0000000000..085061c2b8 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetInProgressTransformer.kt @@ -0,0 +1,34 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.approval + +import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class SetApprovalBottomSheetInProgressTransformer( + private val onDismiss: () -> Unit, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val approvalBottomSheetConfig = prevState.bottomSheetConfig?.content as? GiveTxPermissionBottomSheetConfig + return prevState.copy( + bottomSheetConfig = prevState.bottomSheetConfig?.copy( + onDismissRequest = onDismiss, + isShow = true, + content = approvalBottomSheetConfig?.let { config -> + config.copy( + data = config.data.copy( + approveButton = config.data.approveButton.copy( + enabled = false, + loading = true, + ), + cancelButton = config.data.cancelButton.copy( + enabled = false, + ), + ), + onCancel = onDismiss, + ) + } as TangemBottomSheetConfigContent, + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt new file mode 100644 index 0000000000..9eb4e68640 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt @@ -0,0 +1,78 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.approval + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.bottomsheet.permission.state.* +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.utils.Provider +import com.tangem.utils.transformer.Transformer + +internal class ShowApprovalBottomSheetTransformer( + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val onDismiss: () -> Unit, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val cryptoCurrency = cryptoCurrencyStatusProvider().currency + val cryptoCurrencyValue = cryptoCurrencyStatusProvider().value + + val amountState = prevState.amountState as? AmountState.Data ?: return prevState + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState + val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: return prevState + val feeState = confirmationState.feeState as? FeeState.Content ?: return prevState + + val walletAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value.orEmpty() + val validatorAddress = validatorState.chosenValidator.address + val feeCryptoValue = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = feeState.fee?.amount?.value, + cryptoCurrency = cryptoCurrency, + ) + val feeFiatValue = BigDecimalFormatter.formatFiatAmount( + fiatAmount = feeState.fee?.amount?.maxValue, + fiatCurrencyCode = appCurrencyProvider().code, + fiatCurrencySymbol = appCurrencyProvider().symbol, + ) + return prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismiss, + content = GiveTxPermissionBottomSheetConfig( + data = GiveTxPermissionState.ReadyForRequest( + currency = cryptoCurrency.symbol, + amount = amountState.amountTextField.value, + approveType = ApproveType.LIMITED, + walletAddress = walletAddress, + spenderAddress = validatorAddress, + fee = resourceReference( + R.string.common_crypto_fiat_format, + wrappedList(feeCryptoValue, feeFiatValue), + ), + approveButton = ApprovePermissionButton( + enabled = true, + loading = false, + onClick = prevState.clickIntents::onApprovalClick, + ), + cancelButton = CancelPermissionButton( + enabled = true, + ), + subtitle = resourceReference( + id = R.string.give_permission_staking_subtitle, + formatArgs = wrappedList(cryptoCurrency.symbol), + ), + dialogText = resourceReference(R.string.give_permission_staking_footer), + ), + onCancel = onDismiss, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index d3c1bda086..fe6c923210 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -13,6 +13,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.tangem.common.ui.amountScreen.AmountScreenContent +import com.tangem.common.ui.bottomsheet.permission.GiveTxPermissionBottomSheet +import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig import com.tangem.common.ui.navigationButtons.NavigationButtonsBlock import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig @@ -64,6 +66,7 @@ fun StakingBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { if (bottomSheetConfig == null) return when (bottomSheetConfig.content) { is StakingInfoBottomSheetConfig -> StakingInfoBottomSheet(bottomSheetConfig) + is GiveTxPermissionBottomSheetConfig -> GiveTxPermissionBottomSheet(bottomSheetConfig) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 3734e90f19..db14a50094 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -36,6 +36,10 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onActiveStake(activeStake: BalanceState) + fun showApprovalBottomSheet() + + fun onApprovalClick() + fun onExploreClick() fun onShareClick() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index a5fd8c4d07..dfac50c551 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -11,6 +11,9 @@ import com.tangem.common.extensions.hexToBytes import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -36,9 +39,13 @@ import com.tangem.features.staking.impl.presentation.state.transformers.amount.A import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.approval.ShowApprovalBottomSheetTransformer import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer import com.tangem.utils.Provider 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.ImmutableList import kotlinx.coroutines.flow.* @@ -64,14 +71,16 @@ internal class StakingViewModel @Inject constructor( private val submitHashUseCase: SubmitHashUseCase, private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase, private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase, + private val isApproveNeededUseCase: IsApproveNeededUseCase, + private val clipboardManager: ClipboardManager, + private val vibratorHapticManager: VibratorHapticManager, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { val uiState: StateFlow = stateController.uiState val value: StakingUiState get() = uiState.value - var stakingStateRouter: StakingStateRouter by Delegates.notNull() - private set + private var stakingStateRouter: StakingStateRouter by Delegates.notNull() private val cryptoCurrencyId: CryptoCurrency.ID = savedStateHandle.get(AppRoute.Staking.CRYPTO_CURRENCY_ID_KEY) @@ -92,6 +101,10 @@ internal class StakingViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var appCurrency: AppCurrency by Delegates.notNull() + private var isApprovalNeeded: Boolean = false + + private var approvalJobHolder: JobHolder = JobHolder() + init { subscribeOnSelectedAppCurrency() subscribeOnBalanceHiding() @@ -254,6 +267,29 @@ internal class StakingViewModel @Inject constructor( onNextClick(actionType, activeStake.pendingActions) } + override fun showApprovalBottomSheet() { + stateController.update( + ShowApprovalBottomSheetTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + ) { + stateController.update(DismissBottomSheetStateTransformer()) + }, + ) + } + + override fun onApprovalClick() { + viewModelScope.launch { + stateController.update( + SetApprovalBottomSheetInProgressTransformer { + stateController.update(DismissBottomSheetStateTransformer()) + }, + ) + + // todo staking polygon + }.saveIn(approvalJobHolder) + } + override fun onExploreClick() { val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content @@ -265,7 +301,16 @@ internal class StakingViewModel @Inject constructor( } override fun onShareClick() { - // TODO add hash to clipboard and send analytics event + val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data + val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content + val txUrl = transactionDoneState?.txUrl + + if (txUrl != null) { + vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) + clipboardManager.setText(text = txUrl) + } + + // TODO staking [REDACTED_TASK_KEY] } fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { @@ -273,6 +318,10 @@ internal class StakingViewModel @Inject constructor( this.stakingStateRouter = stateRouter } + private fun setupApprovalNeeded() { + isApprovalNeeded = isApproveNeededUseCase(cryptoCurrencyStatus.currency).getOrElse { false } + } + private fun subscribeOnCurrencyStatusUpdates() { viewModelScope.launch { getUserWalletUseCase(userWalletId).fold( @@ -287,6 +336,8 @@ internal class StakingViewModel @Inject constructor( ifRight = { cryptoCurrencyStatus = it + setupApprovalNeeded() + val networkId = cryptoCurrencyStatus.currency.network.id val isStakeMoreAvailable = isStakeMoreAvailableUseCase(networkId) stateController.update( From e386c3655704c7090bc4961ff00ad2b6597b8781 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 Aug 2024 20:36:12 +0500 Subject: [PATCH 48/87] Updated on 2026-08-14 --- .../ui/amountScreen/utils/FormatterUtils.kt | 18 ------- .../core/ui/utils/BigDecimalFormatter.kt | 52 +++++++++++++++++-- .../presentation/ui/SendNavigationButtons.kt | 34 +++++++----- .../ui/fee/SendSpeedSelectorItem.kt | 11 +++- .../impl/presentation/ui/send/FeeBlock.kt | 13 +++-- .../presentation/ui/block/StakingFeeBlock.kt | 17 ++++-- 6 files changed, 99 insertions(+), 46 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt index 67c6ed8cc9..c578193df0 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt @@ -1,30 +1,12 @@ package com.tangem.common.ui.amountScreen.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.core.ui.utils.BigDecimalFormatter.EMPTY_BALANCE_SIGN import com.tangem.domain.appcurrency.model.AppCurrency import java.math.BigDecimal -private const val CRYPTO_FEE_DECIMALS = 6 - -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.coerceAtMost(CRYPTO_FEE_DECIMALS), - ), - ), - ) -} - fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { if (value == null || rate == null) return null val formattedFiat = getFiatString(value = value, rate = rate, appCurrency = appCurrency) 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 d1d4cf9c8c..b0c1a83fed 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 @@ -13,15 +13,18 @@ import java.text.NumberFormat import java.util.Currency import java.util.Locale +@Suppress("LargeClass") object BigDecimalFormatter { const val EMPTY_BALANCE_SIGN = DASH_SIGN - const val CAN_BE_LOWER_SIGN = LOWER_SIGN + private const val CAN_BE_LOWER_SIGN = LOWER_SIGN private val FORMAT_THRESHOLD = BigDecimal("0.01") private const val TEMP_CURRENCY_CODE = "USD" private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01") + private val CRYPTO_FEE_FORMAT_THRESHOLD = BigDecimal("0.000001") + private const val FIAT_MARKET_DEFAULT_DIGITS = 2 private const val FIAT_MARKET_EXTENDED_DIGITS = 6 @@ -112,6 +115,45 @@ object BigDecimalFormatter { } } + fun formatCryptoFeeAmount( + cryptoAmount: BigDecimal?, + cryptoCurrency: String, + decimals: Int, + canBeLower: Boolean = false, + locale: Locale = Locale.getDefault(), + ): String { + if (cryptoAmount == null) return EMPTY_BALANCE_SIGN + + val formatter = NumberFormat.getNumberInstance(locale).apply { + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + val amountFormatted = if (cryptoAmount.checkCryptoThreshold()) { + buildString { + append(CAN_BE_LOWER_SIGN) + append( + formatter.format(CRYPTO_FEE_FORMAT_THRESHOLD), + ) + } + } else { + buildString { + if (canBeLower) { + append(CAN_BE_LOWER_SIGN) + } + append(formatter.format(cryptoAmount)) + } + } + + return if (cryptoCurrency.isEmpty()) { + amountFormatted + } else { + amountFormatted + "\u2009$cryptoCurrency" + } + } + fun formatCryptoAmount( cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency, @@ -137,7 +179,7 @@ object BigDecimalFormatter { roundingMode = RoundingMode.HALF_UP } - return if (fiatAmount.isLessThanThreshold()) { + return if (fiatAmount.checkFiatThreshold()) { buildString { append(CAN_BE_LOWER_SIGN) append( @@ -160,7 +202,7 @@ object BigDecimalFormatter { if (fiatAmount == null) return EMPTY_BALANCE_SIGN val formatterCurrency = getCurrency(fiatCurrencyCode) - val digits = if (fiatAmount.isLessThanThreshold()) { + val digits = if (fiatAmount.checkFiatThreshold()) { FIAT_MARKET_EXTENDED_DIGITS } else { FIAT_MARKET_DEFAULT_DIGITS @@ -358,7 +400,9 @@ object BigDecimalFormatter { } } - private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD + private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD + + private fun BigDecimal.checkCryptoThreshold() = this > BigDecimal.ZERO && this < CRYPTO_FEE_FORMAT_THRESHOLD @Suppress("MagicNumber") fun getProperFiatPriceDecimals(price: BigDecimal): Int { 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 3e676181c9..18d8413082 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 @@ -22,7 +22,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.ui.SendDoneButtons -import com.tangem.common.ui.amountScreen.utils.getCryptoReference import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.ui.R import com.tangem.core.ui.components.Keyboard @@ -34,6 +33,7 @@ import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.* 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.SendUiCurrentScreen import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType @@ -186,16 +186,6 @@ private fun SendingText( fiatCurrencySymbol = feeState.appCurrency.symbol, fiatCurrencyCode = feeState.appCurrency.code, ) - val feeValue = if (feeState.isFeeConvertibleToFiat) { - getFiatString( - value = feeState.fee?.amount?.value, - rate = feeState.rate, - appCurrency = feeState.appCurrency, - ) - } else { - getCryptoReference(feeState.fee?.amount, feeState.isFeeApproximate)?.resolveReference().orEmpty() - } - val textResource = remember(uiState) { resourceReference( id = if (feeState.isFeeConvertibleToFiat) { @@ -203,7 +193,7 @@ private fun SendingText( } else { R.string.send_summary_transaction_description_no_fiat_fee }, - formatArgs = wrappedList(sendingValue, feeValue), + formatArgs = wrappedList(sendingValue, feeState.getFiatValue()), ) } Text( @@ -219,6 +209,22 @@ private fun SendingText( } } +private fun SendStates.FeeState.getFiatValue() = if (isFeeConvertibleToFiat) { + getFiatString( + value = fee?.amount?.value, + rate = rate, + appCurrency = appCurrency, + ) +} else { + val amount = fee?.amount + BigDecimalFormatter.formatCryptoFeeAmount( + cryptoAmount = amount?.value, + cryptoCurrency = amount?.currencySymbol.orEmpty(), + decimals = amount?.decimals ?: 0, + canBeLower = isFeeApproximate, + ) +} + private fun getButtonData( uiState: SendUiState, currentState: SendUiCurrentScreen, @@ -245,11 +251,11 @@ private fun getButtonData( private fun isButtonEnabled(currentState: SendUiCurrentScreen, uiState: SendUiState): Boolean { return when (currentState.type) { - SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled + SendUiStateType.Amount -> uiState.amountState.isPrimaryButtonEnabled SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled SendUiStateType.Fee -> uiState.feeState?.isPrimaryButtonEnabled SendUiStateType.Send -> uiState.sendState?.isPrimaryButtonEnabled - SendUiStateType.EditAmount -> uiState.editAmountState?.isPrimaryButtonEnabled + SendUiStateType.EditAmount -> uiState.editAmountState.isPrimaryButtonEnabled SendUiStateType.EditRecipient -> uiState.editRecipientState?.isPrimaryButtonEnabled SendUiStateType.EditFee -> uiState.editFeeState?.isPrimaryButtonEnabled else -> true 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 f7aacce75a..712f915547 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 @@ -11,11 +11,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.common.ui.amountScreen.utils.getCryptoReference import com.tangem.common.ui.amountScreen.utils.getFiatReference 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.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseToBigDecimal @@ -52,7 +52,14 @@ internal fun SendSpeedSelectorItem( iconRes = iconRes, onSelect = onSelect, modifier = modifier, - preDot = getCryptoReference(amount, state.isFeeApproximate), + preDot = stringReference( + BigDecimalFormatter.formatCryptoFeeAmount( + cryptoAmount = amount?.value, + cryptoCurrency = amount?.currencySymbol.orEmpty(), + decimals = amount?.decimals ?: 0, + canBeLower = state.isFeeApproximate, + ), + ), postDot = if (state.isFeeConvertibleToFiat) { getFiatReference(amount?.value, state.rate, state.appCurrency) } else { 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 c43df695b9..b396811b81 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 @@ -14,12 +14,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.common.ui.amountScreen.utils.getCryptoReference import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendStates @@ -61,7 +61,14 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, o SelectorRowItem( titleRes = title, iconRes = icon, - preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate), + preDot = stringReference( + BigDecimalFormatter.formatCryptoFeeAmount( + cryptoAmount = feeAmount?.value, + cryptoCurrency = feeAmount?.currencySymbol.orEmpty(), + decimals = feeAmount?.decimals ?: 0, + canBeLower = feeState.isFeeApproximate, + ), + ), postDot = if (feeState.isFeeConvertibleToFiat) { getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency) } else { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index c552ca2fbc..196b47b156 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -16,14 +16,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.common.ui.R -import com.tangem.common.ui.amountScreen.utils.getCryptoReference -import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.staking.impl.presentation.state.FeeState import java.math.BigDecimal @@ -53,7 +53,14 @@ internal fun StakingFeeBlock(feeState: FeeState) { SelectorRowItem( titleRes = title, iconRes = icon, - preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate), + preDot = stringReference( + BigDecimalFormatter.formatCryptoFeeAmount( + cryptoAmount = feeAmount?.value, + cryptoCurrency = feeAmount?.currencySymbol.orEmpty(), + decimals = feeAmount?.decimals ?: 0, + canBeLower = feeState.isFeeApproximate, + ), + ), postDot = if (feeState.isFeeConvertibleToFiat) { getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency) } else { From 434c459728cfa3fac2fb639b33796a19e48f9504 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Aug 2024 15:31:16 +0300 Subject: [PATCH 49/87] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 3 ++ .../java/com/tangem/tap/TangemApplication.kt | 19 ++------ .../common/log/TangemAppLoggerInitializer.kt | 48 +++++++++++++++++++ .../tap/common/log/TangemCardSDKLogger.kt | 16 +------ .../tap/data/TangemBlockchainSDKLogger.kt | 16 +------ .../tap/di/data/BlockchainSDKLoggerModule.kt | 25 ---------- ...LoggerModule.kt => TangemLoggingModule.kt} | 24 +++++++--- .../api/common/config/TangemTech.kt | 20 +------- .../managers/ProdApiConfigsManagerTest.kt | 11 +---- .../settings/DefaultSettingsRepository.kt | 21 ++++++-- .../data/settings/di/SettingsDataModule.kt | 8 +++- .../domain/feedback/FeedbackDataBuilder.kt | 4 +- .../repositories/SettingsRepository.kt | 2 +- 13 files changed, 103 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt delete mode 100644 app/src/main/java/com/tangem/tap/di/data/BlockchainSDKLoggerModule.kt rename app/src/main/java/com/tangem/tap/di/data/{CardSDKLoggerModule.kt => TangemLoggingModule.kt} (59%) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 5c04f34268..ff4810cfc6 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -32,6 +32,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles +import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles import com.tangem.tap.proxy.AppStateHolder @@ -116,4 +117,6 @@ interface ApplicationEntryPoint { fun getAppRouter(): AppRouter fun getPushNotificationsFeatureToggles(): PushNotificationsFeatureToggles + + fun getTangemAppLogger(): TangemAppLoggerInitializer } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 0c1d250993..42280203ef 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -6,8 +6,6 @@ import android.content.pm.PackageManager import coil.ImageLoader import coil.ImageLoaderFactory import com.chuckerteam.chucker.api.ChuckerInterceptor -import com.orhanobut.logger.AndroidLogAdapter -import com.orhanobut.logger.Logger import com.tangem.Log import com.tangem.LogFormat import com.tangem.TangemSdkLogger @@ -54,8 +52,8 @@ import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandle import com.tangem.tap.common.feedback.AdditionalFeedbackInfo import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.common.images.createCoilImageLoader +import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.common.log.TangemLogCollector -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 @@ -69,7 +67,6 @@ import com.tangem.wallet.BuildConfig import dagger.hilt.EntryPoints import kotlinx.coroutines.runBlocking import org.rekotlin.Store -import timber.log.Timber import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository lateinit var store: Store @@ -191,6 +188,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles get() = entryPoint.getPushNotificationsFeatureToggles() + + private val tangemAppLoggerInitializer: TangemAppLoggerInitializer + get() = entryPoint.getTangemAppLogger() // endregion override fun onCreate() { @@ -202,16 +202,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { fun init() { store = createReduxStore() - if (BuildConfig.LOG_ENABLED) { - Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy())) - Timber.plant( - object : Timber.DebugTree() { - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - Logger.log(priority, tag, message, t) - } - }, - ) - } + tangemAppLoggerInitializer.initialize() foregroundActivityObserver = ForegroundActivityObserver() activityResultCaller = foregroundActivityObserver diff --git a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt new file mode 100644 index 0000000000..7a7e2c72b0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt @@ -0,0 +1,48 @@ +package com.tangem.tap.common.log + +import android.util.Log +import com.orhanobut.logger.AndroidLogAdapter +import com.orhanobut.logger.Logger +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.wallet.BuildConfig +import timber.log.Timber + +/** + * Tangem app logger + * + * @property settingsRepository repository for saving logs + * +[REDACTED_AUTHOR] + */ +class TangemAppLoggerInitializer( + private val settingsRepository: SettingsRepository, +) { + + /** Initialize */ + fun initialize() { + if (IS_LOG_ENABLED) { + Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy())) + } + + Timber.plant(tree = createTimberTree()) + } + + private fun createTimberTree(): Timber.Tree { + return object : Timber.DebugTree() { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + if (IS_LOG_ENABLED) { + Logger.log(priority, tag, message, t) + } + + if (PERMITTED_PRIORITY.contains(priority)) { + settingsRepository.saveLogMessage(message) + } + } + } + } + + private companion object { + val IS_LOG_ENABLED: Boolean = BuildConfig.LOG_ENABLED + val PERMITTED_PRIORITY = listOf(Log.ERROR, Log.INFO) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/log/TangemCardSDKLogger.kt b/app/src/main/java/com/tangem/tap/common/log/TangemCardSDKLogger.kt index 0da020d6bf..fad6b562f7 100644 --- a/app/src/main/java/com/tangem/tap/common/log/TangemCardSDKLogger.kt +++ b/app/src/main/java/com/tangem/tap/common/log/TangemCardSDKLogger.kt @@ -4,11 +4,6 @@ import com.tangem.Log import com.tangem.LogFormat import com.tangem.TangemSdkLogger import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock /** * CardSDK logger implementation @@ -16,7 +11,6 @@ import kotlinx.coroutines.sync.withLock * @property levels logging levels * @property messageFormatter message formatter * @property settingsRepository settings repository - * @property dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ @@ -24,19 +18,11 @@ internal class TangemCardSDKLogger( private val levels: List, private val messageFormatter: LogFormat, private val settingsRepository: SettingsRepository, - private val dispatchers: CoroutineDispatcherProvider, ) : TangemSdkLogger { - private val scope = CoroutineScope(dispatchers.main) - private val mutex = Mutex() - override fun log(message: () -> String, level: Log.Level) { if (!levels.contains(level)) return - scope.launch(dispatchers.main) { - mutex.withLock { - settingsRepository.updateAppLogs(message = messageFormatter.format(message, level)) - } - } + settingsRepository.saveLogMessage(message = messageFormatter.format(message, level)) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt b/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt index 903d5542ea..c275de1626 100644 --- a/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt +++ b/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt @@ -2,33 +2,19 @@ package com.tangem.tap.data import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock /** * BlockchainSDK logger implementation * * @property settingsRepository settings repository - * @property dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ internal class TangemBlockchainSDKLogger( private val settingsRepository: SettingsRepository, - private val dispatchers: CoroutineDispatcherProvider, ) : BlockchainSDKLogger { - private val scope = CoroutineScope(dispatchers.main) - private val mutex = Mutex() - override fun log(level: BlockchainSDKLogger.Level, message: String) { - scope.launch(dispatchers.main) { - mutex.withLock { - settingsRepository.updateAppLogs(message) - } - } + settingsRepository.saveLogMessage(message) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/BlockchainSDKLoggerModule.kt b/app/src/main/java/com/tangem/tap/di/data/BlockchainSDKLoggerModule.kt deleted file mode 100644 index 90906e322d..0000000000 --- a/app/src/main/java/com/tangem/tap/di/data/BlockchainSDKLoggerModule.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.di.data - -import com.tangem.blockchain.common.logging.BlockchainSDKLogger -import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.tap.data.TangemBlockchainSDKLogger -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 BlockchainSDKLoggerModule { - - @Provides - @Singleton - fun provideBlockchainSDKLogger( - settingsRepository: SettingsRepository, - dispatchers: CoroutineDispatcherProvider, - ): BlockchainSDKLogger { - return TangemBlockchainSDKLogger(settingsRepository, dispatchers) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/CardSDKLoggerModule.kt b/app/src/main/java/com/tangem/tap/di/data/TangemLoggingModule.kt similarity index 59% rename from app/src/main/java/com/tangem/tap/di/data/CardSDKLoggerModule.kt rename to app/src/main/java/com/tangem/tap/di/data/TangemLoggingModule.kt index 9b326b64c7..e361ca70a6 100644 --- a/app/src/main/java/com/tangem/tap/di/data/CardSDKLoggerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/TangemLoggingModule.kt @@ -3,9 +3,11 @@ package com.tangem.tap.di.data import com.tangem.Log import com.tangem.LogFormat import com.tangem.TangemSdkLogger +import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.common.log.TangemCardSDKLogger -import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.tap.data.TangemBlockchainSDKLogger import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,14 +16,17 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object CardSDKLoggerModule { +internal object TangemLoggingModule { @Provides @Singleton - fun provideCardSDKLogger( - settingsRepository: SettingsRepository, - dispatchers: CoroutineDispatcherProvider, - ): TangemSdkLogger { + fun provideAppLoggerInitializer(settingsRepository: SettingsRepository): TangemAppLoggerInitializer { + return TangemAppLoggerInitializer(settingsRepository) + } + + @Provides + @Singleton + fun provideCardSDKLogger(settingsRepository: SettingsRepository): TangemSdkLogger { val logLevels = listOf( Log.Level.ApduCommand, Log.Level.Apdu, @@ -40,7 +45,12 @@ internal object CardSDKLoggerModule { levels = logLevels, messageFormatter = LogFormat.StairsFormatter(), settingsRepository = settingsRepository, - dispatchers = dispatchers, ) } + + @Provides + @Singleton + fun provideBlockchainSDKLogger(settingsRepository: SettingsRepository): BlockchainSDKLogger { + return TangemBlockchainSDKLogger(settingsRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt index 25d15d3194..b986870d0c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt @@ -1,34 +1,16 @@ package com.tangem.datasource.api.common.config -import com.tangem.datasource.BuildConfig - /** * TangemTech [ApiConfig] * * @property currentEnvironment current api environment */ internal data class TangemTech( - override val currentEnvironment: ApiEnvironment = initializeCurrentEnvironment(), + override val currentEnvironment: ApiEnvironment = ApiEnvironment.PROD, ) : ApiConfig(currentEnvironment) { override val environments: Map = mapOf( ApiEnvironment.DEV to "https://devapi.tangem-tech.com/v1/", ApiEnvironment.PROD to "https://api.tangem-tech.com/v1/", ) - - private companion object { - - fun initializeCurrentEnvironment(): ApiEnvironment { - return when (BuildConfig.BUILD_TYPE) { - DEBUG_BUILD_TYPE, - INTERNAL_BUILD_TYPE, - -> ApiEnvironment.DEV - MOCKED_BUILD_TYPE, - EXTERNAL_BUILD_TYPE, - RELEASE_BUILD_TYPE, - -> ApiEnvironment.PROD - else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") - } - } - } } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index bfc7de29cc..dd71cf6d85 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -61,16 +61,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { private fun createTangemTechModel(): Model { return Model( id = ApiConfig.ID.TangemTech, - expected = when (BuildConfig.BUILD_TYPE) { - DEBUG_BUILD_TYPE, - INTERNAL_BUILD_TYPE, - -> "https://devapi.tangem-tech.com/v1/" - MOCKED_BUILD_TYPE, - EXTERNAL_BUILD_TYPE, - RELEASE_BUILD_TYPE, - -> "https://api.tangem-tech.com/v1/" - else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") - }, + expected = "https://api.tangem-tech.com/v1/", ) } } 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 335d4b86fd..fd7a139ab2 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 @@ -5,12 +5,21 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.joda.time.DateTime internal class DefaultSettingsRepository( private val appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, ) : SettingsRepository { + private val scope = CoroutineScope(dispatchers.io) + private val mutex = Mutex() + override suspend fun shouldShowSaveUserWalletScreen(): Boolean { return appPreferencesStore.getSyncOrDefault( key = PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY, @@ -36,13 +45,17 @@ internal class DefaultSettingsRepository( ) } - override suspend fun updateAppLogs(message: String) { + override fun saveLogMessage(message: String) { val newLogs = DateTime.now().millis.toString() to message - appPreferencesStore.editData { preferences -> - val savedLogs = preferences.getObjectMap(PreferencesKeys.APP_LOGS_KEY) + scope.launch { + mutex.withLock { + appPreferencesStore.editData { preferences -> + val savedLogs = preferences.getObjectMap(PreferencesKeys.APP_LOGS_KEY) - preferences.setObjectMap(key = PreferencesKeys.APP_LOGS_KEY, value = savedLogs + newLogs) + preferences.setObjectMap(key = PreferencesKeys.APP_LOGS_KEY, value = savedLogs + newLogs) + } + } } } diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index d1daa5b8a0..3efdd2e7eb 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -9,6 +9,7 @@ import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.PermissionRepository import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,8 +22,11 @@ internal object SettingsDataModule { @Provides @Singleton - fun provideSettingsRepository(appPreferencesStore: AppPreferencesStore): SettingsRepository { - return DefaultSettingsRepository(appPreferencesStore = appPreferencesStore) + fun provideSettingsRepository( + appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, + ): SettingsRepository { + return DefaultSettingsRepository(appPreferencesStore = appPreferencesStore, dispatchers = dispatchers) } @Provides 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 f69e364662..a3cbd7d4f8 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 @@ -16,8 +16,8 @@ internal class FeedbackDataBuilder { fun addCardInfo(cardInfo: CardInfo) { builder.appendKeyValue("Card ID", cardInfo.cardId) builder.appendKeyValue("Firmware version", cardInfo.firmwareVersion) - builder.appendKeyValue("Linked cards count:", cardInfo.cardsCount) - builder.appendKeyValue("Has seed phrase:", cardInfo.isImported.toString()) + builder.appendKeyValue("Linked cards count", cardInfo.cardsCount) + builder.appendKeyValue("Has seed phrase", cardInfo.isImported.toString()) builder.appendKeyValue("Card Blockchain", cardInfo.cardBlockchain) builder.appendSignedHashes(cardInfo.signedHashesList) } 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 3b5f6d3c40..992990fe85 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 @@ -11,7 +11,7 @@ interface SettingsRepository { suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) @Throws - suspend fun updateAppLogs(message: String) + fun saveLogMessage(message: String) @Throws suspend fun deleteDeprecatedLogs(maxSize: Int) From 42f276c56fa3159d128f8a461977e87bb4d582bf Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Aug 2024 15:57:36 +0300 Subject: [PATCH 50/87] Updated on 2026-08-14 --- .../common/adapter/UnknownEnumMoshiAdapter.kt | 18 ++-- .../response/model/YieldBalanceWrapperDTO.kt | 4 +- .../models/response/model/YieldDTO.kt | 39 ++++++- core/res/src/main/res/values-de/strings.xml | 22 ++-- core/res/src/main/res/values-fr/strings.xml | 4 +- core/res/src/main/res/values-ja/strings.xml | 8 +- .../data/staking/converters/YieldConverter.kt | 29 ++++- .../domain/staking/model/stakekit/Yield.kt | 23 +++- .../repository/MockStakingRepository.kt | 4 +- .../presentation/state/StakingNotification.kt | 16 ++- .../state/StakingStateController.kt | 5 + .../ConfirmationStatePreviewData.kt | 3 +- .../SetConfirmationStateLoadingTransformer.kt | 47 ++++++-- .../SetInitialDataStateTransformer.kt | 102 +++++++++++------- .../state/transformers/SetTitleTransformer.kt | 41 +++++++ .../ui/StakingClaimRewardsValidatorContent.kt | 2 +- .../ui/StakingConfirmationContent.kt | 2 +- .../ui/StakingInitialInfoContent.kt | 13 ++- .../impl/presentation/ui/StakingScreen.kt | 15 +-- .../ui/StakingValidatorListContent.kt | 27 +---- .../presentation/ui/block/StakingFeeBlock.kt | 16 +-- .../viewmodel/StakingViewModel.kt | 2 +- .../components/TokenDetailsActionButton.kt | 15 --- .../TokenDetailsActionButtonsConverter.kt | 6 -- .../components/staking/StakingBalanceBlock.kt | 2 +- 25 files changed, 308 insertions(+), 157 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt index 2e6adf8fe7..f5627c6f96 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt @@ -3,9 +3,11 @@ package com.tangem.datasource.api.common.adapter import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.adapters.EnumJsonAdapter -import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO.BalanceTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO -import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardClaimingDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.RewardTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO @@ -24,13 +26,15 @@ object UnknownEnumMoshiAdapter { fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder { val map = mapOf( + BalanceTypeDTO::class.java to BalanceTypeDTO.UNKNOWN, NetworkTypeDTO::class.java to NetworkTypeDTO.UNKNOWN, - StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN, - YieldDTO.RewardTypeDTO::class.java to YieldDTO.RewardTypeDTO.UNKNOWN, - BalanceDTO.BalanceType::class.java to BalanceDTO.BalanceType.UNKNOWN, - StakingTransactionTypeDTO::class.java to StakingTransactionTypeDTO.UNKNOWN, - StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.UNKNOWN, + RewardClaimingDTO::class.java to RewardClaimingDTO.UNKNOWN, + RewardScheduleDTO::class.java to RewardScheduleDTO.UNKNOWN, + RewardTypeDTO::class.java to RewardTypeDTO.UNKNOWN, StakingActionStatusDTO::class.java to StakingActionStatusDTO.UNKNOWN, + StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN, + StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.UNKNOWN, + StakingTransactionTypeDTO::class.java to StakingTransactionTypeDTO.UNKNOWN, ) return apply { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt index 5be46fc967..1cd438732e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt @@ -19,7 +19,7 @@ data class BalanceDTO( @Json(name = "groupId") val groupId: String, @Json(name = "type") - val type: BalanceType, + val type: BalanceTypeDTO, @Json(name = "amount") val amount: BigDecimal, @Json(name = "date") @@ -38,7 +38,7 @@ data class BalanceDTO( val providerId: String?, ) { - enum class BalanceType { + enum class BalanceTypeDTO { @Json(name = "available") AVAILABLE, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt index bf8956f11f..cff84d1ce7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt @@ -105,13 +105,13 @@ data class YieldDTO( @Json(name = "type") val type: String, @Json(name = "rewardSchedule") - val rewardSchedule: String, + val rewardSchedule: RewardScheduleDTO, @Json(name = "cooldownPeriod") val cooldownPeriod: PeriodDTO, @Json(name = "warmupPeriod") val warmupPeriod: PeriodDTO, @Json(name = "rewardClaiming") - val rewardClaiming: String, + val rewardClaiming: RewardClaimingDTO, @Json(name = "defaultValidator") val defaultValidator: String?, @Json(name = "minimumStake") @@ -135,6 +135,41 @@ data class YieldDTO( @Json(name = "enabled") val enabled: Boolean, ) + + enum class RewardScheduleDTO { + @Json(name = "block") + BLOCK, + + @Json(name = "week") + WEEK, + + @Json(name = "hour") + HOUR, + + @Json(name = "day") + DAY, + + @Json(name = "month") + MONTH, + + @Json(name = "era") + ERA, + + @Json(name = "epoch") + EPOCH, + + UNKNOWN, + } + + enum class RewardClaimingDTO { + @Json(name = "auto") + AUTO, + + @Json(name = "manual") + MANUAL, + + UNKNOWN, + } } enum class RewardTypeDTO { diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 1e9ed93914..e0da81b392 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,7 +1,7 @@ Netzwerk wählen - Token anlegen + Token hinzuf. Token verwalten Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen. So scannt man @@ -88,7 +88,6 @@ Kopieren Adresse kopieren Erstellen - %1$s (%2$s) Eigen %d tag @@ -280,7 +279,7 @@ Rückmeldung Feedback zu Tangem Eine Transaktion kann nicht gesendet werden - Nur diese + diese Transaktion Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. Gib das Genehmigungslimit für das ausgewählte Token an Betrag %s @@ -448,7 +447,7 @@ Möchtest du den Aktivierungsprozess abbrechen? Erste Schritte Für die Karte, die du hinzufügen möchtest, wurde bereits eine andere Wallets erstellt. Wenn du Guthaben auf dieser Wallets hast, hebe es bitte ab, setze diese Karte zurück und füge sie als Backup hinzu. - Backups anlegen + Backups erstellen Lese mehr über die Seed-Phrase leer @@ -501,7 +500,7 @@ Gruppe erstellen Nach Guthaben Token organisieren - Gruppe löschen + Grup. entf. Wählen aus der Galerie aus Einstellungen Du hast keinen Zugriff auf deine Kamera gewährt @@ -640,6 +639,7 @@ Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert %1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. Ungültige Adresse + %1$s (%2$s) Transaktion gesendet Bereite das Scannen der Karte vor, die du einrichten möchtest. Entferne diese Wallet @@ -648,7 +648,7 @@ Aktiv Um deine Kryptos zu unstaken, klick hier. Die Anzahl der zu stakenden Krypros muss mindesten %s betragen - Nicht gestakte beanspruche + nicht gestakte beanspruche Jährliche prozentuale Rendite Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst. Effektiver Jahreszins @@ -672,10 +672,6 @@ Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking. Migrieren Natives Staking - Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden tag. - Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Stunde. - Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden Monat. - Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche. Verdiene Staking-Belohnungen Die Belohnungen werden sofort nach dem unstaken gestoppt. Der unstakingprozess dauert %s. Erneut binden @@ -683,8 +679,8 @@ Belohnungen erneut staken Widerrufen Neuwahl - Automatisch - Händisch + automatisch + händisch Block Tag Täglich @@ -698,7 +694,7 @@ Mehr staken Stake %s Staking beenden%s - Gelocktes unlocken + gelocktes unlocken Unstaken Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen Staking beenden diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 43f28f5375..5e041ee2f3 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -86,7 +86,6 @@ Copier Copier l\'adresse Créer - %1$s (%2$s) Personnalisé %d jour @@ -271,7 +270,7 @@ Commentaires Commentaires sur Tangem Impossible d\'envoyer une transaction - Transaction + Transaction Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange. Spécifiez la limite approuvée pour le jeton sélectionné Montant %s @@ -568,6 +567,7 @@ 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 %1$s est un actif du réseau Tron. Pour calculer les frais et effectuer une transaction, déposez du Tron (TRX) sur votre compte. Adresse incorrecte + %1$s (%2$s) Transaction envoyée Préparez-vous à numériser la carte que vous souhaitez configurer. Oublier le portefeuille diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 56465b3e0b..8ebb799edb 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -87,7 +87,6 @@ コピー アドレスをコピー 作成 - %1$s ( %2$s ) 設定 %d 日 @@ -125,7 +124,6 @@ パスフレーズ ペースト %1$s-%2$s - %1$s — %2$s 続きを読む 受け取る 拒否 @@ -628,6 +626,7 @@ 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 無効なアドレス + %1$s ( %2$s ) 取引が送信されました セットアップしたいカードをスキャンするために準備してください。 ウォレット削除 @@ -637,7 +636,6 @@ 資産のステーキングを解除するには、ここをクリックしてください。 ステーキング金額は %s 以上である必要があります ステーキング解除分を請求する - 年率 ステーキングに参加することで得られる年間収益率。 APR 利用可能 @@ -660,10 +658,6 @@ ステーキングへの参加を有効にするために割り当てられた時間。 移行 ネイティブステーキング - ステーキングにより%1$sを獲得できます。ステーキング報酬は毎日受け取れます。 - ステーキングにより%1$sを獲得できます。ステーキング報酬は1時間ごとに受け取れます。 - ステーキングにより%1$sを獲得できます。ステーキング報酬は毎月受け取れます。 - ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。 ステーキング報酬を獲得 ステーキング解除後、報酬の獲得はすぐに停止します。ステーキング解除プロセスには%sかかります。 再結束 diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index f19e97319c..e383d63f49 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -2,8 +2,10 @@ package com.tangem.data.staking.converters import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO import com.tangem.domain.staking.model.stakekit.AddressArgument import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule import com.tangem.utils.converter.Converter class YieldConverter( @@ -80,10 +82,10 @@ class YieldConverter( token = tokenConverter.convert(metadataDTO.tokenDTO), tokens = metadataDTO.tokensDTO.map { tokenConverter.convert(it) }, type = metadataDTO.type, - rewardSchedule = metadataDTO.rewardSchedule, + rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule), cooldownPeriod = convertPeriod(metadataDTO.cooldownPeriod), warmupPeriod = convertPeriod(metadataDTO.warmupPeriod), - rewardClaiming = metadataDTO.rewardClaiming, + rewardClaiming = convertRewardClaiming(metadataDTO.rewardClaiming), defaultValidator = metadataDTO.defaultValidator, minimumStake = metadataDTO.minimumStake, supportsMultipleValidators = metadataDTO.supportsMultipleValidators, @@ -127,6 +129,29 @@ class YieldConverter( } } + private fun convertRewardSchedule(rewardTypeDTO: RewardScheduleDTO): RewardSchedule { + return when (rewardTypeDTO) { + RewardScheduleDTO.BLOCK -> RewardSchedule.BLOCK + RewardScheduleDTO.WEEK -> RewardSchedule.WEEK + RewardScheduleDTO.HOUR -> RewardSchedule.HOUR + RewardScheduleDTO.DAY -> RewardSchedule.DAY + RewardScheduleDTO.MONTH -> RewardSchedule.MONTH + RewardScheduleDTO.ERA -> RewardSchedule.ERA + RewardScheduleDTO.EPOCH -> RewardSchedule.EPOCH + else -> RewardSchedule.UNKNOWN + } + } + + private fun convertRewardClaiming( + rewardClaimingDTO: YieldDTO.MetadataDTO.RewardClaimingDTO, + ): Yield.Metadata.RewardClaiming { + return when (rewardClaimingDTO) { + YieldDTO.MetadataDTO.RewardClaimingDTO.AUTO -> Yield.Metadata.RewardClaiming.AUTO + YieldDTO.MetadataDTO.RewardClaimingDTO.MANUAL -> Yield.Metadata.RewardClaiming.MANUAL + else -> Yield.Metadata.RewardClaiming.UNKNOWN + } + } + private fun convertArgType(value: String): Yield.Args.ArgType { return when (value) { "address" -> Yield.Args.ArgType.ADDRESS diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt index f30b65fcf4..f8f6b7732d 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt @@ -74,10 +74,10 @@ data class Yield( val token: Token, val tokens: List, val type: String, - val rewardSchedule: String, + val rewardSchedule: RewardSchedule, val cooldownPeriod: Period, val warmupPeriod: Period, - val rewardClaiming: String, + val rewardClaiming: RewardClaiming, val defaultValidator: String?, val minimumStake: Int?, val supportsMultipleValidators: Boolean, @@ -94,6 +94,25 @@ data class Yield( data class Enabled( val enabled: Boolean, ) + + enum class RewardSchedule { + BLOCK, + WEEK, + HOUR, + DAY, + MONTH, + ERA, + EPOCH, + + UNKNOWN, + } + + enum class RewardClaiming { + AUTO, + MANUAL, + + UNKNOWN, + } } enum class RewardType { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index b42fe2c3e4..b2f1a033f3 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -93,10 +93,10 @@ class MockStakingRepository : StakingRepository { ), tokens = listOf(), type = "auto", - rewardSchedule = "1", + rewardSchedule = Yield.Metadata.RewardSchedule.DAY, cooldownPeriod = Yield.Metadata.Period(days = 1), warmupPeriod = Yield.Metadata.Period(days = 1), - rewardClaiming = "1", + rewardClaiming = Yield.Metadata.RewardClaiming.AUTO, defaultValidator = null, minimumStake = null, supportsMultipleValidators = false, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt index 327f1e521c..88936286d3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -4,6 +4,7 @@ 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.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.staking.impl.R internal sealed class StakingNotification(val config: NotificationConfig) { @@ -49,12 +50,23 @@ internal sealed class StakingNotification(val config: NotificationConfig) { ), ) { data class EarnRewards( + val subtitleResourceId: Int, val currencyName: String, - val days: Int, ) : Warning( title = resourceReference(R.string.staking_notification_earn_rewards_title), subtitle = resourceReference( - R.string.common_error, // FIXME + subtitleResourceId, + wrappedList(currencyName), + ), + ) + + data class Unstake( + val cooldownPeriodDays: Int, + ) : Warning( + title = resourceReference(R.string.common_unstake), + subtitle = resourceReference( + R.string.staking_notification_unstake_text, + wrappedList(cooldownPeriodDays, cooldownPeriodDays), ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 2556ac4e2a..1bf70095ff 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.SetTitleTransformer import com.tangem.utils.transformer.Transformer import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -25,20 +26,24 @@ internal class StakingStateController @Inject constructor() { val uiState: StateFlow get() = mutableUiState.asStateFlow() private val buttonsTransformer = SetButtonsStateTransformer() + private val titleTransformer = SetTitleTransformer fun update(function: (StakingUiState) -> StakingUiState) { mutableUiState.update(function = function) mutableUiState.update(function = buttonsTransformer::transform) + mutableUiState.update(function = titleTransformer::transform) } fun update(transformer: Transformer) { mutableUiState.update(function = transformer::transform) mutableUiState.update(function = buttonsTransformer::transform) + mutableUiState.update(function = titleTransformer::transform) } fun clear() { mutableUiState.update { getInitialState() } mutableUiState.update(function = buttonsTransformer::transform) + mutableUiState.update(function = titleTransformer::transform) } private fun getInitialState(): StakingUiState { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index 4fc4fa0cde..807e584ca3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.AmountType.Coin import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -81,7 +82,7 @@ internal object ConfirmationStatePreviewData { notifications = persistentListOf( StakingNotification.Warning.EarnRewards( currencyName = "Solana", - days = 2, + subtitleResourceId = R.string.staking_notification_earn_rewards_text_period_day, ), ), transactionDoneState = TransactionDoneState.Empty, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 04f8fd9de7..1e77078e6e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -1,8 +1,11 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf internal class SetConfirmationStateLoadingTransformer( @@ -24,12 +27,7 @@ internal class SetConfirmationStateLoadingTransformer( chosenValidator = chosenValidator, availableValidators = yield.validators, ), - notifications = persistentListOf( - StakingNotification.Warning.EarnRewards( - currencyName = yield.token.name, - days = yield.metadata.cooldownPeriod.days, - ), - ), + notifications = getNotifications(prevState), footerText = "", transactionDoneState = TransactionDoneState.Empty, pendingActions = persistentListOf(), @@ -38,4 +36,41 @@ internal class SetConfirmationStateLoadingTransformer( ), ) } + + private fun getNotifications(prevState: StakingUiState): ImmutableList { + return persistentListOf( + if (prevState.actionType == StakingActionCommonType.EXIT) { + StakingNotification.Warning.Unstake( + cooldownPeriodDays = yield.metadata.cooldownPeriod.days, + ) + } else { + StakingNotification.Warning.EarnRewards( + currencyName = yield.token.name, + subtitleResourceId = getEarnRewardsPeriod(yield.metadata.rewardSchedule), + ) + }, + ) + } + + private fun getEarnRewardsPeriod(rewardSchedule: Yield.Metadata.RewardSchedule): Int { + return when (rewardSchedule) { + Yield.Metadata.RewardSchedule.BLOCK, + Yield.Metadata.RewardSchedule.DAY, + Yield.Metadata.RewardSchedule.ERA, + Yield.Metadata.RewardSchedule.EPOCH, + -> R.string.staking_notification_earn_rewards_text_period_day + + Yield.Metadata.RewardSchedule.HOUR, + -> R.string.staking_notification_earn_rewards_text_period_hour + + Yield.Metadata.RewardSchedule.WEEK, + -> R.string.staking_notification_earn_rewards_text_period_week + + Yield.Metadata.RewardSchedule.MONTH, + -> R.string.staking_notification_earn_rewards_text_period_month + + else + -> R.string.staking_notification_earn_rewards_text_period_day + } + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 1f330602ec..dc3785b501 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.core.serialization.SerializedBigDecimal import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet @@ -61,6 +62,7 @@ internal class SetInitialDataStateTransformer( R.string.staking_title_stake, wrappedList(cryptoCurrencyStatusProvider().currency.name), ), + cryptoCurrencyName = cryptoCurrencyStatusProvider.invoke().currency.name, clickIntents = clickIntents, currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), @@ -74,7 +76,7 @@ internal class SetInitialDataStateTransformer( private fun createInitialInfoState(): StakingStates.InitialInfoState.Data { return StakingStates.InitialInfoState.Data( isPrimaryButtonEnabled = true, - aprRange = getAprRange(), + aprRange = getAprRange(yield.validators), infoItems = getInfoItems(), onInfoClick = clickIntents::onInfoClick, yieldBalance = yieldBalancesConverter.convert(Unit), @@ -84,24 +86,26 @@ internal class SetInitialDataStateTransformer( private fun getInfoItems(): PersistentList { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance return listOfNotNull( - createAnnualPercentageRateItem(), + createAnnualPercentageRateItem(yield.validators), createAvailableItem(cryptoCurrencyStatus), - createUnbondingPeriodItem(), - createMinimumRequirementItem(cryptoCurrencyStatus), - createRewardClaimingItem(), - createWarmupPeriodItem(), - createRewardScheduleItem(), + createUnbondingPeriodItem(yield.metadata.cooldownPeriod.days), + createMinimumRequirementItem( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum, + ), + createRewardClaimingItem(yield.metadata.rewardClaiming), + createWarmupPeriodItem(yield.metadata.warmupPeriod.days), + createRewardScheduleItem(yield.metadata.rewardSchedule), ).toPersistentList() } - private fun createAnnualPercentageRateItem(): RoundedListWithDividersItemData { + private fun createAnnualPercentageRateItem(validators: List): RoundedListWithDividersItemData { return RoundedListWithDividersItemData( id = R.string.staking_details_annual_percentage_rate, startText = TextReference.Res(R.string.staking_details_annual_percentage_rate), - endText = getAprRange(), + endText = getAprRange(validators), iconClick = { clickIntents.onInfoClick(InfoType.ANNUAL_PERCENTAGE_RATE) }, ) } @@ -120,14 +124,14 @@ internal class SetInitialDataStateTransformer( ) } - private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData { + private fun createUnbondingPeriodItem(cooldownPeriodDays: Int): RoundedListWithDividersItemData { return RoundedListWithDividersItemData( id = R.string.staking_details_unbonding_period, startText = TextReference.Res(R.string.staking_details_unbonding_period), endText = pluralReference( id = R.plurals.common_days, - count = yield.metadata.cooldownPeriod.days, - formatArgs = wrappedList(yield.metadata.cooldownPeriod.days), + count = cooldownPeriodDays, + formatArgs = wrappedList(cooldownPeriodDays), ), iconClick = { clickIntents.onInfoClick(InfoType.UNBONDING_PERIOD) }, ) @@ -135,53 +139,60 @@ internal class SetInitialDataStateTransformer( private fun createMinimumRequirementItem( cryptoCurrencyStatus: CryptoCurrencyStatus, + minimumCryptoAmount: SerializedBigDecimal?, ): RoundedListWithDividersItemData? { - val minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum - return minimumCryptoAmount?.let { - RoundedListWithDividersItemData( - id = R.string.staking_details_minimum_requirement, - startText = TextReference.Res(R.string.staking_details_minimum_requirement), - endText = TextReference.Str( - value = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = it, - cryptoCurrency = cryptoCurrencyStatus.currency.symbol, - decimals = cryptoCurrencyStatus.currency.decimals, - ), + minimumCryptoAmount ?: return null + + return RoundedListWithDividersItemData( + id = R.string.staking_details_minimum_requirement, + startText = TextReference.Res(R.string.staking_details_minimum_requirement), + endText = TextReference.Str( + value = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = minimumCryptoAmount, + cryptoCurrency = cryptoCurrencyStatus.currency.symbol, + decimals = cryptoCurrencyStatus.currency.decimals, ), - ) - } + ), + ) } - private fun createRewardClaimingItem(): RoundedListWithDividersItemData { + private fun createRewardClaimingItem( + rewardClaiming: Yield.Metadata.RewardClaiming, + ): RoundedListWithDividersItemData? { + val endTextId = rewardClaimingResources[rewardClaiming] ?: return null + return RoundedListWithDividersItemData( id = R.string.staking_details_reward_claiming, startText = TextReference.Res(R.string.staking_details_reward_claiming), - endText = TextReference.Str(yield.metadata.rewardClaiming), + endText = TextReference.Res(endTextId), iconClick = { clickIntents.onInfoClick(InfoType.REWARD_CLAIMING) }, ) } - private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? { - val warmupPeriod = yield.metadata.warmupPeriod.days - if (warmupPeriod == 0) return null + private fun createWarmupPeriodItem(warmupPeriodDays: Int): RoundedListWithDividersItemData? { + if (warmupPeriodDays == 0) return null return RoundedListWithDividersItemData( id = R.string.staking_details_warmup_period, startText = TextReference.Res(R.string.staking_details_warmup_period), endText = pluralReference( id = R.plurals.common_days, - count = yield.metadata.warmupPeriod.days, - formatArgs = wrappedList(yield.metadata.warmupPeriod.days), + count = warmupPeriodDays, + formatArgs = wrappedList(warmupPeriodDays), ), iconClick = { clickIntents.onInfoClick(InfoType.WARMUP_PERIOD) }, ) } - private fun createRewardScheduleItem(): RoundedListWithDividersItemData { + private fun createRewardScheduleItem( + rewardSchedule: Yield.Metadata.RewardSchedule, + ): RoundedListWithDividersItemData? { + val endTextId = rewardScheduleResources[rewardSchedule] ?: return null + return RoundedListWithDividersItemData( id = R.string.staking_details_reward_schedule, startText = TextReference.Res(R.string.staking_details_reward_schedule), - endText = TextReference.Str(yield.metadata.rewardSchedule), + endText = TextReference.Res(endTextId), iconClick = { clickIntents.onInfoClick(InfoType.REWARD_SCHEDULE) }, ) } @@ -200,8 +211,8 @@ internal class SetInitialDataStateTransformer( ) } - private fun getAprRange(): TextReference { - val aprValues = yield.validators.mapNotNull { it.apr } + private fun getAprRange(validators: List): TextReference { + val aprValues = validators.mapNotNull { it.apr } val minApr = aprValues.min() val maxApr = aprValues.max() @@ -223,5 +234,22 @@ internal class SetInitialDataStateTransformer( companion object { private val EQUALITY_THRESHOLD = BigDecimal(1E-10) + + private val rewardScheduleResources = mapOf( + Yield.Metadata.RewardSchedule.BLOCK to R.string.staking_reward_schedule_each_day, + Yield.Metadata.RewardSchedule.WEEK to R.string.staking_reward_schedule_week, + Yield.Metadata.RewardSchedule.HOUR to R.string.staking_reward_schedule_hour, + Yield.Metadata.RewardSchedule.DAY to R.string.staking_reward_schedule_each_day, + Yield.Metadata.RewardSchedule.MONTH to R.string.staking_reward_schedule_month, + Yield.Metadata.RewardSchedule.ERA to R.string.staking_reward_schedule_era, + Yield.Metadata.RewardSchedule.EPOCH to R.string.staking_reward_schedule_epoch, + Yield.Metadata.RewardSchedule.UNKNOWN to null, + ) + + private val rewardClaimingResources = mapOf( + Yield.Metadata.RewardClaiming.MANUAL to R.string.staking_reward_claiming_manual, + Yield.Metadata.RewardClaiming.AUTO to R.string.staking_reward_claiming_auto, + Yield.Metadata.RewardSchedule.UNKNOWN to null, + ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt new file mode 100644 index 0000000000..ffb66a5ee1 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt @@ -0,0 +1,41 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.utils.transformer.Transformer + +internal object SetTitleTransformer : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + val actionType = prevState.actionType + val currentStep = prevState.currentStep + + val title = when { + currentStep == StakingStep.Amount -> { + resourceReference(R.string.send_amount_label) + } + + currentStep == StakingStep.Validators -> { + resourceReference(R.string.staking_validators) + } + + actionType == StakingActionCommonType.EXIT && currentStep != StakingStep.InitialInfo -> { + resourceReference( + R.string.staking_title_unstake, + wrappedList(prevState.cryptoCurrencyName), + ) + } + else -> { + resourceReference( + R.string.staking_title_stake, + wrappedList(prevState.cryptoCurrencyName), + ) + } + } + + return prevState.copy(title = title) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt index 6657db14f0..49d711da2d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt @@ -31,7 +31,7 @@ internal fun StakingClaimRewardsValidatorContent( if (state !is StakingStates.RewardsValidatorsState.Data) return Column( modifier = Modifier // Do not put fillMaxSize() in here - .background(TangemTheme.colors.background.tertiary) + .background(TangemTheme.colors.background.secondary) .padding(horizontal = TangemTheme.dimens.spacing12) .verticalScroll(rememberScrollState()), ) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 476c27a1ca..ae5079b938 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -40,7 +40,7 @@ internal fun StakingConfirmationContent( Column( modifier = Modifier - .background(TangemTheme.colors.background.tertiary) + .background(TangemTheme.colors.background.secondary) .padding(horizontal = TangemTheme.dimens.spacing16) .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 51bb3380b1..810f49be28 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.* +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -107,14 +108,16 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl @Composable private fun BannerBlock(onClick: () -> Unit) { Box( - modifier = Modifier.clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(), - onClick = onClick, - ), + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = onClick, + ), ) { Image( modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.FillWidth, painter = painterResource(R.drawable.img_staking_banner), contentDescription = null, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index fe6c923210..ef94fa0c1d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import com.tangem.common.ui.amountScreen.AmountScreenContent import com.tangem.common.ui.bottomsheet.permission.GiveTxPermissionBottomSheet import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig @@ -36,7 +35,7 @@ internal fun StakingScreen(uiState: StakingUiState) { BackHandler(onBack = uiState.clickIntents::onPrevClick) Column( modifier = Modifier - .background(color = TangemTheme.colors.background.tertiary) + .background(color = TangemTheme.colors.background.secondary) .fillMaxSize() .imePadding() .systemBarsPadding(), @@ -72,14 +71,6 @@ fun StakingBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { @Composable private fun SendAppBar(uiState: StakingUiState) { - val titleRes = when (uiState.currentStep) { - StakingStep.Amount -> stringResource(id = R.string.send_amount_label) - StakingStep.InitialInfo, - StakingStep.RewardsValidators, - StakingStep.Validators, - StakingStep.Confirmation, - -> uiState.title.resolveReference() - } val backIcon = when (uiState.currentStep) { StakingStep.Amount, StakingStep.Validators, @@ -94,10 +85,10 @@ private fun SendAppBar(uiState: StakingUiState) { } } AppBarWithBackButtonAndIcon( - text = titleRes, + text = uiState.title.resolveReference(), backIconRes = backIcon, onBackClick = uiState.clickIntents::onBackClick, - backgroundColor = TangemTheme.colors.background.tertiary, + backgroundColor = TangemTheme.colors.background.secondary, modifier = Modifier.height(TangemTheme.dimens.size56), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt index 743979fa67..fe98e24c83 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -4,12 +4,10 @@ import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle @@ -47,23 +45,6 @@ internal fun StakingValidatorListContent( contentPadding = PaddingValues(bottom = bottomBarHeight), modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { - item(key = "HEADER") { - Text( - text = stringResource(R.string.staking_validator), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .fillMaxWidth() - .clip(CornersToRound.TOP_2.getShape()) - .background(TangemTheme.colors.background.action) - .padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing8, - ), - ) - } if (state is ValidatorState.Content) { val validators = state.availableValidators items( @@ -93,10 +74,10 @@ internal fun StakingValidatorListContent( onSelect = { clickIntents.onValidatorSelect(item) }, modifier = Modifier .clip( - if (index == validators.lastIndex) { - CornersToRound.BOTTOM_2 - } else { - CornersToRound.ZERO + when (index) { + 0 -> CornersToRound.TOP_2 + validators.lastIndex -> CornersToRound.BOTTOM_2 + else -> CornersToRound.ZERO }.getShape(), ) .background(TangemTheme.colors.background.action), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index c552ca2fbc..2aaa4189cb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -88,7 +88,7 @@ private fun BoxScope.FeeLoading(feeState: FeeState) { RectangleShimmer( radius = TangemTheme.dimens.radius3, modifier = Modifier.size( - height = TangemTheme.dimens.size12, + height = TangemTheme.dimens.size24, width = TangemTheme.dimens.size90, ), ) @@ -107,7 +107,7 @@ private fun BoxScope.FeeError(feeState: FeeState) { Text( text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body2, + style = TangemTheme.typography.body1, ) } } @@ -117,7 +117,7 @@ private fun BoxScope.FeeError(feeState: FeeState) { @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: FeeState.Content) { +private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: FeeState) { TangemThemePreview { StakingFeeBlock( feeState = value, @@ -125,11 +125,13 @@ private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) va } } -private class FeeBlockPreviewProvider : PreviewParameterProvider { +private class FeeBlockPreviewProvider : PreviewParameterProvider { - override val values: Sequence + override val values: Sequence get() = sequenceOf( - feeState, + contentState, + FeeState.Loading, + FeeState.Error, ) private val fee = Fee.Common( @@ -141,7 +143,7 @@ private class FeeBlockPreviewProvider : PreviewParameterProvider Unit) : TokenDetailsActionButton( - config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_stake), - iconResId = R.drawable.ic_staking_24, - onClick = onClick, - dimContent = dimContent, - ), - ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt index afe2413ce6..dbf8a99f3c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt @@ -38,12 +38,6 @@ internal class TokenDetailsActionButtonsConverter( onLongClick = clickIntents::onCopyAddress, ) } - is TokenActionsState.ActionState.Stake -> { - TokenDetailsActionButton.Stake( - dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, - onClick = { clickIntents.onStakeClick(action.unavailabilityReason) }, - ) - } is TokenActionsState.ActionState.Sell -> { TokenDetailsActionButton.Sell( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index 0d5d72efd5..c1ce6e3abd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -33,7 +33,7 @@ internal fun StakingBalanceBlock(state: StakingBlockUM.Staked, modifier: Modifie modifier = modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) + .background(TangemTheme.colors.background.primary) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), From ca7088e2f5a65a9da03905b298dba4df6727e4fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Aug 2024 17:05:35 +0300 Subject: [PATCH 51/87] Updated on 2026-08-14 --- .../com/tangem/tap/LockUserWalletsTimer.kt | 14 +++--- .../tap/common/extensions/Navigation.kt | 8 ++-- .../tangem/tap/common/redux/LogMiddleware.kt | 8 +--- .../GeneralUserWalletsListManager.kt | 4 +- .../DefaultLegacyWalletConnectRepository.kt | 46 +++++++++---------- .../domain/WalletConnectInteractor.kt | 10 ++-- .../com/tangem/tap/routing/ProxyAppRouter.kt | 6 +++ .../deeplink/impl/DefaultDeepLinksRegistry.kt | 6 +-- .../data/common/api/ApiResponseRaise.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 30 ++++++++++-- .../feature/swap/viewmodels/SwapViewModel.kt | 3 +- .../viewmodels/WalletsUpdateActionResolver.kt | 2 +- .../DefaultBlockchainSDKFactory.kt | 4 +- .../WalletManagerFactoryCreator.kt | 2 +- 14 files changed, 87 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 8758b045e3..18f3cff503 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -35,7 +35,7 @@ internal class LockUserWalletsTimer( val wasApplicationStopped = settingsRepository.wasApplicationStopped() val shouldOpenWelcomeScreenOnResume = settingsRepository.shouldOpenWelcomeScreenOnResume() - Timber.d( + Timber.i( """ Owner resumed |- Was stopped: $wasApplicationStopped @@ -55,7 +55,7 @@ internal class LockUserWalletsTimer( } override fun onStop(owner: LifecycleOwner) { - Timber.d("Owner stopped") + Timber.i("Owner stopped") owner.lifecycleScope.launch { settingsRepository.setWasApplicationStopped(value = true) @@ -63,13 +63,13 @@ internal class LockUserWalletsTimer( } override fun onDestroy(owner: LifecycleOwner) { - Timber.d("Owner destroyed") + Timber.i("Owner destroyed") stop() } fun restart() { if (delayJob == null) return - Timber.d( + Timber.i( """ Timer restart |- Duration millis: ${duration.inWholeMilliseconds} @@ -80,7 +80,7 @@ internal class LockUserWalletsTimer( private fun start(log: Boolean = true) { if (log) { - Timber.d( + Timber.i( """ Timer start |- Duration millis: ${duration.inWholeMilliseconds} @@ -92,7 +92,7 @@ internal class LockUserWalletsTimer( private fun stop(log: Boolean = true) { if (log) { - Timber.d( + Timber.i( """ Timer stop |- Was started: ${delayJob?.isActive ?: false} @@ -114,7 +114,7 @@ internal class LockUserWalletsTimer( val currentTime = System.currentTimeMillis() val wasApplicationStopped = settingsRepository.wasApplicationStopped() - Timber.d( + Timber.i( """ Finished |- App is stopped: $wasApplicationStopped 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 5d7a14709f..243a2f8770 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 @@ -12,12 +12,12 @@ fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider val currentFragmentName = getBackStackEntryAt(backStackEntryCount - 1).name if (name == currentFragmentName) { - Timber.d("Fragment $name is already at the top of the stack") + Timber.i("Fragment $name is already at the top of the stack") return } } - Timber.d("Showing $name route") + Timber.i("Showing $name route") val isPoppedBack = popBackStackImmediate(name, 0) @@ -30,9 +30,9 @@ fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider fragment.showFragment(fragmentManager = this, name) } - Timber.d("Route $name is shown") + Timber.i("Route $name is shown") } else { - Timber.d("Route $name is found in backstack and shown") + Timber.i("Route $name is found in backstack and shown") } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt index 30b127fda1..21cca072f7 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt @@ -1,19 +1,15 @@ package com.tangem.tap.common.redux -import com.tangem.domain.common.LogConfig import org.rekotlin.Middleware import timber.log.Timber /** [REDACTED_AUTHOR] */ -val logMiddleware: Middleware = { dispatch, appState -> +val logMiddleware: Middleware = { _, _ -> { nextDispatch -> { action -> - if (LogConfig.storeAction) { - Timber.d("Dispatch action: $action") - // printOnboardingWalletState() - } + Timber.i("Dispatch action: $action") nextDispatch(action) } } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt index 2901b7120b..4cad89ba5d 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt @@ -165,10 +165,10 @@ internal class GeneralUserWalletsListManager( } if (possibleManager == implementation.value) { - Timber.e("Switch to same manager ${possibleManager::class.simpleName}") + Timber.e("Switch to the same manager ${possibleManager::class.simpleName}") } - Timber.d("Switch to ${possibleManager::class.simpleName}") + Timber.i("Switch to ${possibleManager::class.simpleName}") val previousManager = implementation.value implementation.value = copySelectedUserWallet( diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index cea126bf75..8f80642201 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -62,7 +62,7 @@ internal class DefaultLegacyWalletConnectRepository( application = application, metaData = appMetaData, ) { error -> - Timber.d("Error while initializing client: $error") + Timber.e("Error while initializing client: $error") scope.launch { _events.emit( WalletConnectEvents.SessionApprovalError( @@ -73,7 +73,7 @@ internal class DefaultLegacyWalletConnectRepository( } Web3Wallet.initialize(Wallet.Params.Init(core = CoreClient)) { error -> - Timber.d("Error while initializing Web3Wallet: $error") + Timber.e("Error while initializing Web3Wallet: $error") scope.launch { _events.emit( WalletConnectEvents.SessionApprovalError( @@ -94,11 +94,11 @@ internal class DefaultLegacyWalletConnectRepository( verifyContext: Wallet.Model.VerifyContext, ) { // Triggered when wallet receives the session proposal sent by a Dapp - Timber.d("sessionProposal: $sessionProposal") + Timber.i("sessionProposal: $sessionProposal") this@DefaultLegacyWalletConnectRepository.sessionProposal = sessionProposal if (sessionProposal.name in unsupportedDApps) { - Timber.w("Unsupported DApp") + Timber.i("Unsupported DApp") scope.launch { _events.emit( WalletConnectEvents.SessionApprovalError( @@ -115,7 +115,7 @@ internal class DefaultLegacyWalletConnectRepository( ) if (missingNetworks.isNotEmpty()) { - Timber.w("Not added blockchains: $missingNetworks") + Timber.i("Not added blockchains: $missingNetworks") scope.launch { _events.emit( WalletConnectEvents.SessionApprovalError( @@ -150,12 +150,12 @@ internal class DefaultLegacyWalletConnectRepository( verifyContext: Wallet.Model.VerifyContext, ) { // Triggered when a Dapp sends SessionRequest to sign a transaction or a message - Timber.d("sessionRequest: $sessionRequest") + Timber.i("sessionRequest: $sessionRequest") val request = wcRequestDeserializer.deserialize( method = sessionRequest.request.method, params = sessionRequest.request.params, ) - Timber.d("sessionRequestParsed: $request") + Timber.i("sessionRequestParsed: $request") when (request) { is WcRequest.AddChain -> { @@ -193,7 +193,7 @@ internal class DefaultLegacyWalletConnectRepository( verifyContext: Wallet.Model.VerifyContext, ) { // Triggered when Dapp / Requester makes an authorization request - Timber.d("onAuthRequest: $authRequest") + Timber.i("onAuthRequest: $authRequest") } override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { @@ -204,12 +204,12 @@ internal class DefaultLegacyWalletConnectRepository( updateSessionsInternal().join() } } - Timber.d("onSessionDelete: $sessionDelete") + Timber.i("onSessionDelete: $sessionDelete") } override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { // Triggered when wallet receives the session settlement response from Dapp - Timber.d("onSessionSettleResponse: $settleSessionResponse") + Timber.i("onSessionSettleResponse: $settleSessionResponse") if (settleSessionResponse is Wallet.Model.SettledSessionResponse.Result) { scope.launch { _events.emit( @@ -225,19 +225,19 @@ internal class DefaultLegacyWalletConnectRepository( override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) { // Triggered when wallet receives the session update response from Dapp - Timber.d("onSessionUpdateResponse: $sessionUpdateResponse") + Timber.i("onSessionUpdateResponse: $sessionUpdateResponse") updateSessionsInternal() } override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) { // Triggered whenever the connection state is changed - Timber.d("onConnectionStateChange: $state") + Timber.i("onConnectionStateChange: $state") if (state.isAvailable) updateSessionsInternal() } override fun onError(error: Wallet.Model.Error) { // Triggered whenever there is an issue inside the SDK - Timber.d("onError: $error") + Timber.i("onError: $error") } } } @@ -299,12 +299,12 @@ internal class DefaultLegacyWalletConnectRepository( }, ) - Timber.d("Session approval is prepared for sending: $sessionApproval") + Timber.i("Session approval is prepared for sending: $sessionApproval") Web3Wallet.approveSession( params = sessionApproval, onSuccess = { - Timber.d("Approved successfully: $it") + Timber.i("Approved successfully: $it") analyticsHandler.send( WalletConnect.NewSessionEstablished( dAppName = sessionProposal.name, @@ -313,7 +313,7 @@ internal class DefaultLegacyWalletConnectRepository( ) }, onError = { - Timber.d("Error while approving: $it") + Timber.e("Error while approving: $it") scope.launch { _events.emit( WalletConnectEvents.SessionApprovalError( @@ -370,10 +370,10 @@ internal class DefaultLegacyWalletConnectRepository( ), ), onSuccess = { response -> - Timber.d("Session request responded successfully: $response") + Timber.i("Session request responded successfully: $response") }, onError = { error -> - Timber.d(error.throwable, "Error while responging session request") + Timber.e(error.throwable, "Error while responging session request") WalletConnect.RequestHandledParams( dAppName = session?.name ?: "", @@ -427,10 +427,10 @@ internal class DefaultLegacyWalletConnectRepository( reason = "", ), onSuccess = { - Timber.d("Rejected successfully: $it") + Timber.i("Rejected successfully: $it") }, onError = { - Timber.d("Error while rejecting: $it") + Timber.e("Error while rejecting: $it") }, ) } @@ -447,10 +447,10 @@ internal class DefaultLegacyWalletConnectRepository( ), ) updateSessionsInternal() - Timber.d("Disconnected successfully: $it") + Timber.i("Disconnected successfully: $it") }, onError = { - Timber.d("Error while disconnecting: $it") + Timber.e("Error while disconnecting: $it") }, ) } @@ -483,7 +483,7 @@ internal class DefaultLegacyWalletConnectRepository( url = it.metaData?.url, ) } - Timber.d("Available sessions: $availableSessions") + Timber.i("Available sessions: $availableSessions") currentSessions = availableSessions _activeSessions.emit(availableSessions) } 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 1f5dce9663..122a9ed76f 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 @@ -35,7 +35,7 @@ class WalletConnectInteractor( val blockchainHelper: WcBlockchainHelper, ) { - var isWalletConnectReadyForDeepLinks = false + private var isWalletConnectReadyForDeepLinks = false private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) { GetSelectedWalletUseCase(userWalletsListManager) @@ -82,7 +82,7 @@ class WalletConnectInteractor( private suspend fun initWithWallet(userWallet: UserWallet) { if (userWallet.isMultiCurrency) { - Timber.d("WalletConnect: initialize and setup networks for ${userWallet.walletId}") + Timber.i("WalletConnect: initialize and setup networks for ${userWallet.walletId}") startListeningWc(userWallet.walletId.stringValue, getCardId(userWallet)) subscribeOnCurrenciesUpdates(userWallet) } @@ -143,9 +143,9 @@ class WalletConnectInteractor( private suspend fun subscribeToEvents() { events .onEach { wcEvent -> + Timber.i("WalletConnect: event: $wcEvent") when (wcEvent) { is WalletConnectEvents.SessionProposal -> { - Timber.d("WC session proposal event received") val unsupportedNetworks = wcEvent.requiredChainIds .filter { blockchainHelper.chainIdToNetworkIdOrNull(it) == null } if (unsupportedNetworks.isNotEmpty()) { @@ -230,6 +230,7 @@ class WalletConnectInteractor( } fun approveSessionProposal(accounts: List) { + Timber.i("Approve session proposal: $accounts") val userNamespaces: Map> = accounts .groupBy { account -> blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId) @@ -241,14 +242,17 @@ class WalletConnectInteractor( } fun rejectSessionProposal() { + Timber.i("Reject session proposal") walletConnectRepository.reject() } fun disconnectSession(topic: String) { + Timber.i("Disconnect session: $topic") walletConnectRepository.disconnect(topic) } fun cancelRequest(topic: String, id: Long) { + Timber.i("Cancel request: $topic, $id") walletConnectRepository.cancelRequest(topic, id) } diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt index aca4ce8efb..592656008e 100644 --- a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -7,6 +7,7 @@ import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import timber.log.Timber import kotlin.reflect.KClass internal class ProxyAppRouter( @@ -31,30 +32,35 @@ internal class ProxyAppRouter( override fun push(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { routerScope.launch(dispatchers.mainImmediate) { + Timber.i("Push route: $route") innerRouter.push(route, onComplete) } } override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { routerScope.launch(dispatchers.mainImmediate) { + Timber.i("Replace all routes with $routes") innerRouter.replaceAll(*routes, onComplete = onComplete) } } override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { routerScope.launch(dispatchers.mainImmediate) { + Timber.i("Pop route") innerRouter.pop(onComplete) } } override fun popTo(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { routerScope.launch(dispatchers.mainImmediate) { + Timber.i("Pop to route: $route") innerRouter.popTo(route, onComplete) } } override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { routerScope.launch(dispatchers.mainImmediate) { + Timber.i("Pop to route class: $routeClass") innerRouter.popTo(routeClass, onComplete) } } diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt index bf84d3e382..dcd5c85434 100644 --- a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt @@ -18,7 +18,7 @@ internal class DefaultDeepLinksRegistry : DeepLinksRegistry { val received = intent.data ?: return false var hasMatch = false - Timber.d( + Timber.i( """ Received deep link intent |- Received URI: $received @@ -33,7 +33,7 @@ internal class DefaultDeepLinksRegistry : DeepLinksRegistry { val params = getParams(expected, received) - Timber.d( + Timber.i( """ Matched deep link |- Expected URI: $expected @@ -45,7 +45,7 @@ internal class DefaultDeepLinksRegistry : DeepLinksRegistry { } if (!hasMatch) { - Timber.d( + Timber.i( """ No match found for deep link |- Received URI: $received diff --git a/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt b/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt index 837e169c8b..3334a3c339 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt @@ -67,7 +67,7 @@ suspend inline fun safeApiCall( ): T = recover( block = { call(ApiResponseRaise(raise = this)) }, recover = { - Timber.w(it, "Unable to perform safe API call") + Timber.e(it, "Unable to perform safe API call") onError(it) }, ) \ 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 184a6886ca..c0d6a4c835 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 @@ -268,6 +268,17 @@ internal class SwapInteractorImpl @Inject constructor( amountToSwap: String, selectedFee: FeeType, ): Map { + Timber.i( + """ + Find the best quote + |- fromToken: $fromToken + |- toToken: $toToken + |- providers: $providers + |- amountToSwap: $amountToSwap + |- selectedFee: $selectedFee + """.trimIndent(), + ) + return providers.map { provider -> val amountDecimal = toBigDecimalOrNull(amountToSwap) if (amountDecimal == null || amountDecimal.signum() == 0) { @@ -311,7 +322,7 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, isBalanceWithoutFeeEnough: Boolean, ): Pair { - val quotes = repository.findBestQuote( + val maybeQuotes = repository.findBestQuote( fromContractAddress = fromToken.currency.getContractAddress(), fromNetwork = fromToken.currency.network.backendId, toContractAddress = toToken.currency.getContractAddress(), @@ -324,7 +335,7 @@ internal class SwapInteractorImpl @Inject constructor( ) val fromTokenAddress = getTokenAddress(fromToken.currency) - val isAllowedToSpend = quotes.fold( + val isAllowedToSpend = maybeQuotes.fold( ifRight = { quotes -> quotes.allowanceContract?.let { isAllowedToSpend(networkId, fromToken.currency, amount, it) @@ -352,7 +363,7 @@ internal class SwapInteractorImpl @Inject constructor( } else { provider to getQuotesState( provider = provider, - quoteDataModel = quotes, + quoteDataModel = maybeQuotes, amount = amount, fromToken = fromToken, toToken = toToken, @@ -573,6 +584,19 @@ internal class SwapInteractorImpl @Inject constructor( includeFeeInAmount: IncludeFeeInAmount, fee: TxFee, ): SwapTransactionState { + Timber.i( + """ + Swap + |- swapProvider: $swapProvider + |- swapData: $swapData + |- currencyToSend: $currencyToSend + |- currencyToGet: $currencyToGet + |- amountToSwap: $amountToSwap + |- includeFeeInAmount: $includeFeeInAmount + |- fee: $fee + """.trimIndent(), + ) + val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError if (isDemoCardUseCase(cardId)) return SwapTransactionState.DemoMode 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 f02139cb42..eb128c84db 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 @@ -208,7 +208,7 @@ internal class SwapViewModel @Inject constructor( ) } }.onFailure { - Timber.tag(loggingTag).e(it) + Timber.e(it) applyInitialTokenChoice( state = TokensDataStateExpress.EMPTY, @@ -1212,7 +1212,6 @@ internal class SwapViewModel @Inject constructor( } private companion object { - const val loggingTag = "SwapViewModel" const val INITIAL_AMOUNT = "" const val UPDATE_DELAY = 10000L const val DEBOUNCE_AMOUNT_DELAY = 1000L diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt index aeb7ea5c03..26d3d0c945 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -35,7 +35,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( getUpdateContentAction(currentState, wallets, selectedWallet) } - Timber.d("Resolved action: $action") + Timber.i("Resolved action: $action") return action } diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt index 6207c86026..8fb67b9107 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -74,7 +74,7 @@ internal class DefaultBlockchainSDKFactory( return@launch } - Timber.d("Update BlockchainSDKConfig") + Timber.i("Update BlockchainSDKConfig") configStore.store( value = BlockchainSDKConfigConverter.convert(value = config), @@ -91,7 +91,7 @@ internal class DefaultBlockchainSDKFactory( return@launch } - Timber.d("Update BlockchainProviderTypes") + Timber.i("Update BlockchainProviderTypes") blockchainProviderTypesStore.store( value = BlockchainProviderTypesConverter.convert(response), diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index 9ede47a471..e7224f8434 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -27,7 +27,7 @@ internal class WalletManagerFactoryCreator @Inject constructor( ) { fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { - Timber.d("Create WalletManagerFactory") + Timber.i("Create WalletManagerFactory") return WalletManagerFactory( config = config, From 2ffe05e93a1267a8dde31dcfe17eba700ae4205b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Aug 2024 11:32:26 +0300 Subject: [PATCH 52/87] Updated on 2026-08-14 --- .../datasource/api/common/config/ApiConfig.kt | 15 +++----- .../datasource/api/common/config/Express.kt | 14 +++----- .../api/common/config/TangemTech.kt | 12 +++---- .../config/managers/DevApiConfigsManager.kt | 35 +++++++++---------- .../managers/MutableApiConfigsManager.kt | 4 +-- .../config/managers/ProdApiConfigsManager.kt | 4 +-- .../EnvironmentsTogglesViewModel.kt | 10 +++--- 7 files changed, 39 insertions(+), 55 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index 7c5b88f27a..ba2cc575a9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -3,13 +3,14 @@ package com.tangem.datasource.api.common.config /** * Api config * - * @property currentEnvironment current api environment - * * @see API configuration * [REDACTED_AUTHOR] */ -sealed class ApiConfig(open val currentEnvironment: ApiEnvironment) { +sealed class ApiConfig { + + /** Default environment */ + abstract val defaultEnvironment: ApiEnvironment /** Available environments with base url */ abstract val environments: Map @@ -22,14 +23,6 @@ sealed class ApiConfig(open val currentEnvironment: ApiEnvironment) { TangemTech, } - /** Copy method for sealed class [ApiConfig] */ - fun copySealed(currentEnvironment: ApiEnvironment): ApiConfig { - return when (this) { - is Express -> copy(currentEnvironment = currentEnvironment) - is TangemTech -> copy(currentEnvironment = currentEnvironment) - } - } - private fun initializeId(): ID { return when (this) { is Express -> ID.Express diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt index 5ebf06c017..be5bba3816 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -2,14 +2,10 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig -/** - * Express [ApiConfig] - * - * @property currentEnvironment current api environment - */ -internal data class Express( - override val currentEnvironment: ApiEnvironment = initializeCurrentEnvironment(), -) : ApiConfig(currentEnvironment) { +/** Express [ApiConfig] */ +internal class Express : ApiConfig() { + + override val defaultEnvironment: ApiEnvironment = getInitialEnvironment() override val environments: Map = mapOf( ApiEnvironment.DEV to "[REDACTED_ENV_URL]", @@ -19,7 +15,7 @@ internal data class Express( private companion object { - fun initializeCurrentEnvironment(): ApiEnvironment { + fun getInitialEnvironment(): ApiEnvironment { return when (BuildConfig.BUILD_TYPE) { DEBUG_BUILD_TYPE -> ApiEnvironment.DEV INTERNAL_BUILD_TYPE, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt index b986870d0c..b1491bb852 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt @@ -1,13 +1,9 @@ package com.tangem.datasource.api.common.config -/** - * TangemTech [ApiConfig] - * - * @property currentEnvironment current api environment - */ -internal data class TangemTech( - override val currentEnvironment: ApiEnvironment = ApiEnvironment.PROD, -) : ApiConfig(currentEnvironment) { +/** TangemTech [ApiConfig] */ +internal class TangemTech : ApiConfig() { + + override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD override val environments: Map = mapOf( ApiEnvironment.DEV to "https://devapi.tangem-tech.com/v1/", diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt index 0af38d7ba9..5a1c92008d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt @@ -7,10 +7,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* /** * Implementation of [ApiConfigsManager] in DEV environment @@ -23,9 +20,9 @@ internal class DevApiConfigsManager( private val dispatchers: CoroutineDispatcherProvider, ) : MutableApiConfigsManager { - override val configs: Flow> get() = _apiConfigs + override val configs: Flow> get() = _apiConfigs - private val _apiConfigs = MutableStateFlow(value = ApiConfig.values()) + private val _apiConfigs = MutableStateFlow(value = ApiConfig.values().associateWith { it.defaultEnvironment }) override suspend fun initialize() { // We can't use appPreferencesStore.getObjectMap as base flow, @@ -33,13 +30,11 @@ internal class DevApiConfigsManager( // See [getBaseUrl] appPreferencesStore.getObjectMap(PreferencesKeys.apiConfigsEnvironmentKey) .onEach { savedEnvironments -> - _apiConfigs.value = _apiConfigs.value.map { config -> - val savedEnvironment = savedEnvironments[config.id.name] + _apiConfigs.update { apiConfigs -> + apiConfigs.mapValues { + val (config, currentEnvironment) = it - if (savedEnvironment != null) { - config.copySealed(currentEnvironment = savedEnvironment) - } else { - config + savedEnvironments[config.id.name] ?: currentEnvironment } } } @@ -47,14 +42,16 @@ internal class DevApiConfigsManager( } override fun getBaseUrl(id: ApiConfig.ID): String { - val config = _apiConfigs.value.firstOrNull { it.id == id } - ?: error("Api config with id [$id] not found. Check ApiConfig implementations") + val apiConfigs = _apiConfigs.value - return config.environments[config.currentEnvironment] - ?: error( - "Api config with id [$id] doesn't contain environment [${config.currentEnvironment}]. " + - "Check ApiConfig implementations", - ) + val config = apiConfigs.map { it }.firstOrNull { it.key.id == id }?.key + ?: error("Api config with id [$id] not found") + + val currentEnvironment = apiConfigs[config] + ?: error("Current environment of api config with id [$id] not found") + + return config.environments[currentEnvironment] + ?: error("Api config with id [$id] doesn't contain environment [$currentEnvironment]") } override suspend fun changeEnvironment(id: String, environment: ApiEnvironment) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MutableApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MutableApiConfigsManager.kt index 87ec3f21fb..35107ad6d8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MutableApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MutableApiConfigsManager.kt @@ -11,8 +11,8 @@ import kotlinx.coroutines.flow.Flow */ interface MutableApiConfigsManager : ApiConfigsManager { - /** Configs */ - val configs: Flow> + /** Api configs with current [ApiEnvironment] */ + val configs: Flow> /** Change api environment [environment] by [id] */ suspend fun changeEnvironment(id: String, environment: ApiEnvironment) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt index 9f5e1e31e5..34a04a0053 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt @@ -9,10 +9,10 @@ internal class ProdApiConfigsManager : ApiConfigsManager { val config = ApiConfig.values().firstOrNull { it.id == id } ?: error("Api config with id [$id] not found. Check ApiConfig implementations") - return config.environments[config.currentEnvironment] + return config.environments[config.defaultEnvironment] ?: error( "Api config with id [$id] doesn't contain " + - "environment [${config.currentEnvironment}]. Check ApiConfig implementations", + "environment [${config.defaultEnvironment}]. Check ApiConfig implementations", ) } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt index 1b4b896c3e..84cfee3b6f 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt @@ -62,12 +62,14 @@ internal class EnvironmentsTogglesViewModel @Inject constructor( .launchIn(viewModelScope) } - private fun List.toUiModel(): ImmutableSet { - return map { config -> + private fun Map.toUiModel(): ImmutableSet { + return map { + val (config, currentEnvironment) = it + EnvironmentTogglesScreenUM.ApiInfoUM( name = config.id.name, - select = config.currentEnvironment.name, - url = config.environments[config.currentEnvironment] + select = currentEnvironment.name, + url = config.environments[currentEnvironment] ?: error("Current environment's url isn't found"), environments = config.environments .map { environment -> environment.key.name } From f1238f5ef8a4d3429cb85dd080d46aa639ecc822 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 Aug 2024 22:01:26 +0500 Subject: [PATCH 53/87] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 12 + .../tap/di/domain/TransactionDomainModule.kt | 14 ++ .../data/staking/DefaultStakingRepository.kt | 43 +++- .../DefaultTransactionRepository.kt | 72 +++++- domain/staking/build.gradle.kts | 5 +- .../domain/staking/model/StakingApproval.kt | 8 + ...GetConstructedStakingTransactionUseCase.kt | 25 ++ .../staking/GetStakingTransactionUseCase.kt | 9 +- .../domain/staking/IsApproveNeededUseCase.kt | 5 +- .../staking/repositories/StakingRepository.kt | 17 +- domain/tokens/build.gradle.kts | 3 + .../repository/MockStakingRepository.kt | 15 +- .../transaction/TransactionRepository.kt | 26 +- .../CreateApprovalTransactionUseCase.kt | 35 +++ .../usecase/CreateTransactionUseCase.kt | 22 +- .../usecase/GetAllowanceUseCase.kt | 26 ++ ...firmationStateAssentApprovalTransformer.kt | 45 ++++ .../viewmodel/StakingViewModel.kt | 223 ++++++++++++++---- .../feature/swap/DefaultSwapRepository.kt | 9 +- gradle/dependencies.toml | 2 +- 20 files changed, 528 insertions(+), 88 deletions(-) create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingApproval.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceUseCase.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index b4f55832a9..a714d71200 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -168,4 +168,16 @@ internal object StakingDomainModule { stakingErrorResolver = stakingErrorResolver, ) } + + @Provides + @Singleton + fun provideGetConstructedStakingTransactionUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): GetConstructedStakingTransactionUseCase { + return GetConstructedStakingTransactionUseCase( + stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 8af69450c8..73c0236567 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -100,4 +100,18 @@ internal object TransactionDomainModule { ): IsUtxoConsolidationAvailableUseCase { return IsUtxoConsolidationAvailableUseCase(walletManagersFacade) } + + @Provides + @Singleton + fun provideCreateApproveTransactionUseCase( + transactionRepository: TransactionRepository, + ): CreateApprovalTransactionUseCase { + return CreateApprovalTransactionUseCase(transactionRepository) + } + + @Provides + @Singleton + fun provideGetAllowanceUseCase(transactionRepository: TransactionRepository): GetAllowanceUseCase { + return GetAllowanceUseCase(transactionRepository) + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 8d89311bdd..9ce502abe3 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -3,7 +3,11 @@ package com.tangem.data.staking import android.util.Base64 import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionStatus +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toCompressedPublicKey import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.staking.converters.* @@ -25,6 +29,7 @@ import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lceFlow +import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata @@ -231,14 +236,26 @@ internal class DefaultStakingRepository( } } - override suspend fun constructTransaction(transactionId: String): StakingTransaction { + override suspend fun constructTransaction( + networkId: String, + fee: Fee, + transactionId: String, + ): Pair { return withContext(dispatchers.io) { val transactionResponse = stakeKitApi.constructTransaction( transactionId = transactionId, body = ConstructTransactionRequestBody(), ) - transactionConverter.convert(transactionResponse.getOrThrow()) + val transaction = transactionConverter.convert(transactionResponse.getOrThrow()) + val unsignedTransaction = transaction.unsignedTransaction ?: error("No unsigned transaction available") + val transactionData = TransactionData.Compiled( + value = getTransactionDataType(networkId, unsignedTransaction), + fee = fee, + status = TransactionStatus.Unconfirmed, + ) + + transaction to transactionData } } @@ -530,10 +547,24 @@ internal class DefaultStakingRepository( } } - override fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean { + override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval { return when (cryptoCurrency.id.getIntegrationKey()) { - Blockchain.Ethereum.id.plus(Blockchain.Polygon.toCoinId()) -> true - else -> false + Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() -> { + StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER) + } + else -> StakingApproval.Empty + } + } + + private fun getTransactionDataType(networkId: String, unsignedTransaction: String): TransactionData.Compiled.Data { + val blockchain = Blockchain.fromId(networkId) + return when (blockchain) { + Blockchain.Solana, + Blockchain.Cosmos, + -> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes()) + Blockchain.Ethereum, + -> TransactionData.Compiled.Data.RawString(unsignedTransaction) + else -> error("Unsupported blockchain") } } @@ -579,6 +610,8 @@ internal class DefaultStakingRepository( const val NEAR_INTEGRATION_ID = "near-near-native-staking" const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" + const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908" + // uncomment items as implementation is ready val integrationIdMap = mapOf( Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID, diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 56922c5bf0..87f16d07be 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -16,6 +16,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.extensions.hexToBytes import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.models.TransactionType @@ -24,6 +25,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber +import java.math.BigDecimal import java.math.BigInteger import com.tangem.blockchain.blockchains.tron.TransactionType as SdkTransactionType @@ -42,15 +44,15 @@ internal class DefaultTransactionRepository( network: Network, txExtras: TransactionExtras?, hash: String?, - ): TransactionData.Uncompiled? = withContext(coroutineDispatcherProvider.io) { + ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { val blockchain = Blockchain.fromId(network.id.value) val walletManager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, blockchain = blockchain, derivationPath = network.derivationPath.value, - ) + ) ?: error("Wallet manager not found") - return@withContext walletManager?.createTransactionDataInternal( + return@withContext walletManager.createTransactionDataInternal( amount = amount, fee = fee, memo = memo, @@ -61,6 +63,48 @@ internal class DefaultTransactionRepository( ) } + override suspend fun createApprovalTransaction( + amount: Amount, + fee: Fee, + contractAddress: String, + spenderAddress: String, + userWalletId: UserWalletId, + network: Network, + hash: String?, + ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) ?: error("Wallet manager not found") + val approver = walletManager as? Approver ?: error("Cannot cast to Approver") + + val approvalData = approver.getApproveData( + spenderAddress = spenderAddress, + value = amount, + ) + + val extras = createTransactionDataExtras( + data = approvalData, + network = network, + transactionType = TransactionType.APPROVE, + nonce = null, + gasLimit = null, + ) + + return@withContext createTransaction( + amount = amount, + fee = fee, + memo = null, + destination = contractAddress, + userWalletId = userWalletId, + network = network, + txExtras = extras, + hash = hash, + ) + } + override suspend fun validateTransaction( amount: Amount, fee: Fee?, @@ -141,6 +185,28 @@ internal class DefaultTransactionRepository( } } + override suspend fun getAllowance( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency.Token, + spenderAddress: String, + ): BigDecimal { + val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, cryptoCurrency.network) + val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) + val allowanceResult = (walletManager as? Approver)?.getAllowance( + spenderAddress, + Token( + symbol = blockchain.currency, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) ?: error("Cannot cast to Approver") + + return allowanceResult.fold( + onSuccess = { it }, + onFailure = { error(it) }, + ) + } + private fun convertToSdkTransactionType(transactionType: TransactionType): SdkTransactionType { return when (transactionType) { TransactionType.APPROVE -> SdkTransactionType.APPROVE diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index e92bc23b4c..dd1083b53b 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -9,7 +9,6 @@ android { namespace = "com.tangem.domain.staking" } - dependencies { api(projects.domain.staking.models) @@ -20,4 +19,8 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.features.staking.api) + + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingApproval.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingApproval.kt new file mode 100644 index 0000000000..52056c1f11 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingApproval.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.staking.model + +sealed class StakingApproval { + + data class Needed(val spenderAddress: String) : StakingApproval() + + data object Empty : StakingApproval() +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt new file mode 100644 index 0000000000..9e31da097f --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.staking.repositories.StakingRepository + +class GetConstructedStakingTransactionUseCase( + private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + + suspend operator fun invoke( + networkId: String, + fee: Fee, + transactionId: String, + ): Either> = Either.catch { + stakingRepository.constructTransaction(networkId, fee, transactionId) + }.mapLeft { + stakingErrorResolver.resolve(it) + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt index cfe2c9a254..16ac583d13 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt @@ -22,19 +22,14 @@ class GetStakingTransactionUseCase( userWalletId: UserWalletId, network: Network, params: ActionParams, - ): Either { + ): Either> { return Either.catch { val createAction = stakingRepository.createAction(userWalletId, network, params) // workaround, sometimes transaction is not created immediately after actions/enter delay(PATCH_TRANSACTION_REQUEST_DELAY) - val createdTransaction = createAction.transactions - ?.get(createAction.currentStepIndex) - ?: error("No available transaction to patch") - val patchedTransaction = stakingRepository.constructTransaction(createdTransaction.id) - - patchedTransaction + createAction.transactions ?: error("No available transaction to patch") }.mapLeft { stakingErrorResolver.resolve(it) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt index d183b36a2f..10bc37b3fa 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.staking import arrow.core.Either +import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository @@ -10,9 +11,9 @@ class IsApproveNeededUseCase( private val stakingRepository: StakingRepository, private val stakingErrorResolver: StakingErrorResolver, ) { - operator fun invoke(cryptoCurrency: CryptoCurrency): Either { + operator fun invoke(cryptoCurrency: CryptoCurrency): Either { return Either - .catch { stakingRepository.isApproveNeeded(cryptoCurrency) } + .catch { stakingRepository.getStakingApproval(cryptoCurrency) } .mapLeft { stakingErrorResolver.resolve(it) } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index b80b7b95d2..5b6604423e 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -1,7 +1,12 @@ package com.tangem.domain.staking.repositories +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.staking.model.* +import com.tangem.domain.staking.model.StakingApproval +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList @@ -66,7 +71,11 @@ interface StakingRepository { suspend fun estimateGas(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingGasEstimate - suspend fun constructTransaction(transactionId: String): StakingTransaction + suspend fun constructTransaction( + networkId: String, + fee: Fee, + transactionId: String, + ): Pair suspend fun submitHash(transactionId: String, transactionHash: String) @@ -77,6 +86,6 @@ interface StakingRepository { /** Returns whether additional staking is possible if there is already active staking */ fun isStakeMoreAvailable(networkId: Network.ID): Boolean - /** Returns whether approve is needed before staking */ - fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean + /** Returns staking approval */ + fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 989a7e9c5d..bbe01868f9 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -43,4 +43,7 @@ dependencies { /** Tests */ testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) + testImplementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index b42fe2c3e4..bfc852c858 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -1,7 +1,11 @@ package com.tangem.domain.tokens.repository +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionStatus +import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lceFlow +import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata @@ -209,7 +213,11 @@ class MockStakingRepository : StakingRepository { ) } - override suspend fun constructTransaction(transactionId: String): StakingTransaction = StakingTransaction( + override suspend fun constructTransaction( + networkId: String, + fee: Fee, + transactionId: String, + ): Pair = StakingTransaction( id = "id", network = NetworkType.SOLANA, status = StakingTransactionStatus.SIGNED, @@ -224,6 +232,9 @@ class MockStakingRepository : StakingRepository { explorerUrl = null, ledgerHwAppId = null, isMessage = false, + ) to TransactionData.Compiled( + value = TransactionData.Compiled.Data.RawString(""), + status = TransactionStatus.Unconfirmed, ) override suspend fun submitHash(transactionId: String, transactionHash: String) { @@ -240,5 +251,5 @@ class MockStakingRepository : StakingRepository { override fun isStakeMoreAvailable(networkId: Network.ID): Boolean = true - override fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean = true + override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval = StakingApproval.Empty } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 3fb7cbf0b5..6784c1726f 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -1,11 +1,16 @@ package com.tangem.domain.transaction -import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.CommonSigner +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionExtras import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionSendResult +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.models.TransactionType import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal import java.math.BigInteger interface TransactionRepository { @@ -20,7 +25,18 @@ interface TransactionRepository { network: Network, txExtras: TransactionExtras?, hash: String?, - ): TransactionData.Uncompiled? + ): TransactionData.Uncompiled + + @Suppress("LongParameterList") + suspend fun createApprovalTransaction( + amount: Amount, + fee: Fee, + contractAddress: String, + spenderAddress: String, + userWalletId: UserWalletId, + network: Network, + hash: String?, + ): TransactionData.Uncompiled @Suppress("LongParameterList") suspend fun validateTransaction( @@ -49,4 +65,10 @@ interface TransactionRepository { nonce: BigInteger?, gasLimit: BigInteger?, ): TransactionExtras + + suspend fun getAllowance( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency.Token, + spenderAddress: String, + ): BigDecimal } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt new file mode 100644 index 0000000000..01cf52d680 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal + +class CreateApprovalTransactionUseCase( + private val transactionRepository: TransactionRepository, +) { + + @Suppress("LongParameterList") + suspend operator fun invoke( + cryptoCurrency: CryptoCurrency.Token, + userWalletId: UserWalletId, + amount: BigDecimal, + fee: Fee, + contractAddress: String, + spenderAddress: String, + hash: String? = null, + ) = Either.catch { + transactionRepository.createApprovalTransaction( + amount = amount.convertToAmount(cryptoCurrency), + contractAddress = contractAddress, + spenderAddress = spenderAddress, + userWalletId = userWalletId, + network = cryptoCurrency.network, + fee = fee, + hash = hash, + ) + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt index ab5172eaf8..87f10ea630 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt @@ -26,17 +26,15 @@ class CreateTransactionUseCase( txExtras: TransactionExtras? = null, hash: String? = null, ) = Either.catch { - requireNotNull( - transactionRepository.createTransaction( - amount = amount, - fee = fee, - memo = memo, - destination = destination, - userWalletId = userWalletId, - network = network, - txExtras = txExtras, - hash = hash, - ), - ) { "Failed to create transaction" } + transactionRepository.createTransaction( + amount = amount, + fee = fee, + memo = memo, + destination = destination, + userWalletId = userWalletId, + network = network, + txExtras = txExtras, + hash = hash, + ) } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceUseCase.kt new file mode 100644 index 0000000000..316a1061ef --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal + +class GetAllowanceUseCase( + private val transactionRepository: TransactionRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + spenderAddress: String, + ): Either { + return Either.catch { + transactionRepository.getAllowance( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency as CryptoCurrency.Token, + spenderAddress = spenderAddress, + ) + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt new file mode 100644 index 0000000000..c3962befb2 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt @@ -0,0 +1,45 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.approval + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.Provider +import com.tangem.utils.transformer.Transformer + +internal class SetConfirmationStateAssentApprovalTransformer( + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val fee: TransactionFee, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + confirmationState = prevState.confirmationState.copyWrapped(), + bottomSheetConfig = null, + ) + } + + private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState { + return if (this is StakingStates.ConfirmationState.Data) { + copy( + innerState = InnerConfirmationStakingState.ASSENT, + feeState = FeeState.Content( + fee = fee.normal, + rate = cryptoCurrencyStatusProvider().value.fiatRate, + isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate, + appCurrency = appCurrencyProvider(), + isFeeApproximate = false, + ), + validatorState = validatorState.copySealed(isClickable = true), + isPrimaryButtonEnabled = true, + isApprovalNeeded = true, + ) + } else { + this + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index dfac50c551..f3bf4fa6d1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData -import com.tangem.common.extensions.hexToBytes +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.amountScreen.models.AmountState @@ -18,15 +18,20 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.staking.* +import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase +import com.tangem.domain.transaction.usecase.GetAllowanceUseCase +import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.models.UserWallet @@ -40,6 +45,7 @@ import com.tangem.features.staking.impl.presentation.state.transformers.amount.A import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.ShowApprovalBottomSheetTransformer import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer import com.tangem.utils.Provider @@ -51,6 +57,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates @@ -64,6 +71,7 @@ internal class StakingViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getStakingTransactionUseCase: GetStakingTransactionUseCase, + private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase, private val estimateGasUseCase: EstimateGasUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, @@ -71,6 +79,9 @@ internal class StakingViewModel @Inject constructor( private val submitHashUseCase: SubmitHashUseCase, private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase, private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase, + private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, + private val getAllowanceUseCase: GetAllowanceUseCase, + private val getFeeUseCase: GetFeeUseCase, private val isApproveNeededUseCase: IsApproveNeededUseCase, private val clipboardManager: ClipboardManager, private val vibratorHapticManager: VibratorHapticManager, @@ -101,7 +112,7 @@ internal class StakingViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var appCurrency: AppCurrency by Delegates.notNull() - private var isApprovalNeeded: Boolean = false + private var stakingApproval: StakingApproval = StakingApproval.Empty private var approvalJobHolder: JobHolder = JobHolder() @@ -121,7 +132,7 @@ internal class StakingViewModel @Inject constructor( } stakingStateRouter.onNextClick() if (isAssentState()) { - estimateGas(pendingActions) + getFee(pendingActions) } } @@ -139,6 +150,9 @@ internal class StakingViewModel @Inject constructor( value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: error("No validator provided") + val amountState = value.amountState as? AmountState.Data ?: error("No amount provided") + val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value") + val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") val stakingTransaction = getStakingTransactionUseCase( userWalletId = userWalletId, @@ -146,8 +160,7 @@ internal class StakingViewModel @Inject constructor( params = ActionParams( actionCommonType = value.actionType, integrationId = yield.id, - amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value - ?: error("No amount provided"), + amount = amountValue, address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: error("No available address"), validatorAddress = validatorState.chosenValidator.address, @@ -159,19 +172,27 @@ internal class StakingViewModel @Inject constructor( error(it) } - stakingTransaction.unsignedTransaction?.let { - sendStakingTransaction( - transactionId = stakingTransaction.id, - gasEstimate = stakingTransaction.gasEstimate ?: error("No gas estimate available"), - txData = TransactionData.Compiled(value = it.hexToBytes()), - pendingActionList = confirmationState.pendingActions, - ) - } ?: error("No unsigned transaction available") + stakingTransaction + .filterNot { it.type == StakingTransactionType.APPROVAL } + .forEach { transaction -> + val (constructedTransaction, transactionData) = getConstructedStakingTransactionUseCase( + networkId = cryptoCurrencyStatus.currency.network.id.value, + fee = fee, + transactionId = transaction.id, + ).getOrNull() ?: error("No constructed transaction") + + sendStakingTransaction( + transactionId = constructedTransaction.id, + gasEstimate = constructedTransaction.gasEstimate ?: error("No gas estimate available"), + txData = transactionData, + pendingActionList = confirmationState.pendingActions, + ) + } } } } - private fun estimateGas(pendingActions: ImmutableList) { + private fun getFee(pendingActions: ImmutableList) { viewModelScope.launch { stateController.update( SetConfirmationStateLoadingTransformer( @@ -184,38 +205,99 @@ internal class StakingViewModel @Inject constructor( val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: error("No validator provided") - val pendingAction = pendingActions.firstOrNull() - val stakingGasEstimate = estimateGasUseCase( - userWalletId = userWalletId, - network = cryptoCurrencyStatus.currency.network, - params = ActionParams( - actionCommonType = value.actionType, - integrationId = yield.id, - amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value - ?: error("No amount provided"), - address = cryptoCurrencyValue.networkAddress?.defaultAddress?.value - ?: error("No available address"), - validatorAddress = validatorState.chosenValidator.address, - token = yield.token, - passthrough = pendingAction?.passthrough, - type = pendingAction?.type, - ), - ).getOrElse { - stateController.update(AddStakingErrorTransformer(it)) - return@launch - } + val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided") + val sourceAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value + ?: error("No available address") + val validatorAddress = validatorState.chosenValidator.address - stateController.update( - SetConfirmationStateAssentTransformer( - appCurrencyProvider = Provider { appCurrency }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - stakingGasEstimate = stakingGasEstimate, - pendingActionList = pendingActions, - ), - ) + val approval = stakingApproval as? StakingApproval.Needed + if (approval != null) { + val allowance = getAllowanceUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + spenderAddress = approval.spenderAddress, + ).getOrElse { BigDecimal.ZERO } + + if (allowance < amount) { + getApproveFee( + amount = amount, + validatorAddress = validatorAddress, + ) + } else { + estimateGas( + pendingActions = pendingActions, + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + ) + } + } else { + estimateGas( + pendingActions = pendingActions, + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + ) + } } } + private suspend fun estimateGas( + pendingActions: ImmutableList, + amount: BigDecimal, + sourceAddress: String, + validatorAddress: String, + ) { + val pendingAction = pendingActions.firstOrNull() + val stakingGasEstimate = estimateGasUseCase( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + params = ActionParams( + actionCommonType = value.actionType, + integrationId = yield.id, + amount = amount, + address = sourceAddress, + validatorAddress = validatorAddress, + token = yield.token, + passthrough = pendingAction?.passthrough, + type = pendingAction?.type, + ), + ).getOrElse { + stateController.update(AddStakingErrorTransformer(it)) + return + } + + stateController.update( + SetConfirmationStateAssentTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + stakingGasEstimate = stakingGasEstimate, + pendingActionList = pendingActions, + ), + ) + } + + private suspend fun getApproveFee(amount: BigDecimal, validatorAddress: String) { + val approvalFee = getFeeUseCase( + amount = amount, + destination = validatorAddress, + userWallet = userWallet, + cryptoCurrency = cryptoCurrencyStatus.currency, + ).getOrElse { + // TODO staking error + return + } + + stateController.update( + SetConfirmationStateAssentApprovalTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + fee = approvalFee, + ), + ) + } + override fun onPrevClick() { stakingStateRouter.onPrevClick() } @@ -286,7 +368,60 @@ internal class StakingViewModel @Inject constructor( }, ) - // todo staking polygon + val tokenCryptoCurrency = + cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: error("No token currency") + val amountValue = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided") + + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + ?: error("No confirmation state") + val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") + val approval = stakingApproval as? StakingApproval.Needed ?: error("No staking approve spender address") + + val approvalTransaction = createApprovalTransactionUseCase( + amount = amountValue, + contractAddress = tokenCryptoCurrency.contractAddress, + spenderAddress = approval.spenderAddress, + fee = fee, + cryptoCurrency = tokenCryptoCurrency, + userWalletId = userWalletId, + ).fold( + ifLeft = { error -> + Timber.e(error.toString()) + stateController.update( + SetConfirmationStateAssentApprovalTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + fee = TransactionFee.Single(fee), + ), + ) + // TODO staking error + return@launch + }, + ifRight = { it }, + ) + + sendTransactionUseCase( + txData = approvalTransaction, + userWallet = userWallet, + network = tokenCryptoCurrency.network, + ).fold( + ifLeft = { error -> + Timber.e(error.toString()) + stateController.update( + SetConfirmationStateAssentApprovalTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + fee = TransactionFee.Single(fee), + ), + ) + // TODO staking error + }, + ifRight = { + getFee(confirmationState.pendingActions) + stateController.update(DismissBottomSheetStateTransformer()) + }, + ) }.saveIn(approvalJobHolder) } @@ -319,7 +454,7 @@ internal class StakingViewModel @Inject constructor( } private fun setupApprovalNeeded() { - isApprovalNeeded = isApproveNeededUseCase(cryptoCurrencyStatus.currency).getOrElse { false } + stakingApproval = isApproveNeededUseCase(cryptoCurrencyStatus.currency).getOrElse { StakingApproval.Empty } } private fun subscribeOnCurrencyStatusUpdates() { 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 edd4767043..48724f9b77 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 @@ -7,7 +7,6 @@ import arrow.core.raise.either import arrow.core.right import com.squareup.moshi.Moshi import com.tangem.blockchain.common.* -import com.tangem.blockchain.extensions.Result import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.ApiResponse @@ -362,10 +361,10 @@ internal class DefaultSwapRepository @Inject constructor( ), ) ?: error("Cannot cast to Approver") - return when (result) { - is Result.Success -> result.data - is Result.Failure -> error(result.error) - } + return result.fold( + onSuccess = { it }, + onFailure = { error(it) }, + ) } override suspend fun getApproveData( diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index e7873d004f..6201894fe4 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-733" +tangemBlockchainSdk = "develop-734" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From f2b49df6514f8869307f85d0c042d188b346b785 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Aug 2024 21:21:45 +0300 Subject: [PATCH 54/87] Updated on 2026-08-14 --- .../datasource/api/common/config/ApiConfig.kt | 5 ++-- .../config/managers/DevApiConfigsManager.kt | 5 +++- .../config/managers/ProdApiConfigsManager.kt | 13 ++++++++--- .../tangem/datasource/di/ApiConfigsModule.kt | 23 +++++++++++++++++++ .../com/tangem/datasource/di/NetworkModule.kt | 6 +++-- .../managers/ProdApiConfigsManagerTest.kt | 11 +++++++-- .../state/EnvironmentTogglesScreenUM.kt | 2 -- .../ui/EnvironmentTogglesScreen.kt | 12 ---------- .../EnvironmentsTogglesViewModel.kt | 3 +-- 9 files changed, 53 insertions(+), 27 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index ba2cc575a9..693da3fc00 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -1,5 +1,7 @@ package com.tangem.datasource.api.common.config +typealias ApiConfigs = Set<@JvmSuppressWildcards ApiConfig> + /** * Api config * @@ -36,8 +38,5 @@ sealed class ApiConfig { internal const val MOCKED_BUILD_TYPE = "mocked" internal const val EXTERNAL_BUILD_TYPE = "external" internal const val RELEASE_BUILD_TYPE = "release" - - /** All api configs */ - fun values() = listOf(Express(), TangemTech()) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt index 5a1c92008d..4c5f663879 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.api.common.config.managers import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.ApiEnvironment import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -12,17 +13,19 @@ import kotlinx.coroutines.flow.* /** * Implementation of [ApiConfigsManager] in DEV environment * + * @param apiConfigs api configs * @property appPreferencesStore app preferences store * @property dispatchers coroutine dispatcher provider */ internal class DevApiConfigsManager( + apiConfigs: ApiConfigs, private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) : MutableApiConfigsManager { override val configs: Flow> get() = _apiConfigs - private val _apiConfigs = MutableStateFlow(value = ApiConfig.values().associateWith { it.defaultEnvironment }) + private val _apiConfigs = MutableStateFlow(value = apiConfigs.associateWith { it.defaultEnvironment }) override suspend fun initialize() { // We can't use appPreferencesStore.getObjectMap as base flow, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt index 34a04a0053..e9785199e6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt @@ -1,12 +1,19 @@ package com.tangem.datasource.api.common.config.managers import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiConfigs -/** Implementation of [ApiConfigsManager] in PROD environment */ -internal class ProdApiConfigsManager : ApiConfigsManager { +/** + * Implementation of [ApiConfigsManager] in PROD environment + * + * @property apiConfigs api configs + */ +internal class ProdApiConfigsManager( + private val apiConfigs: ApiConfigs, +) : ApiConfigsManager { override fun getBaseUrl(id: ApiConfig.ID): String { - val config = ApiConfig.values().firstOrNull { it.id == id } + val config = apiConfigs.firstOrNull { it.id == id } ?: error("Api config with id [$id] not found. Check ApiConfig implementations") return config.environments[config.defaultEnvironment] diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt new file mode 100644 index 0000000000..f2bb6573a6 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.Express +import com.tangem.datasource.api.common.config.TangemTech +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet + +@Module +@InstallIn(SingletonComponent::class) +internal object ApiConfigsModule { + + @Provides + @IntoSet + fun provideExpressConfig(): ApiConfig = Express() + + @Provides + @IntoSet + fun provideTangemTechConfig(): ApiConfig = TangemTech() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 19a3e4c6b1..b636e47a19 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -4,6 +4,7 @@ import android.content.Context import com.squareup.moshi.Moshi import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager @@ -42,13 +43,14 @@ class NetworkModule { @Provides @Singleton fun provideApiConfigManager( + apiConfigs: ApiConfigs, appPreferencesStore: AppPreferencesStore, dispatchers: CoroutineDispatcherProvider, ): ApiConfigsManager { return if (BuildConfig.TESTER_MENU_ENABLED) { - DevApiConfigsManager(appPreferencesStore, dispatchers) + DevApiConfigsManager(apiConfigs, appPreferencesStore, dispatchers) } else { - ProdApiConfigsManager() + ProdApiConfigsManager(apiConfigs) } } diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index dd71cf6d85..a2701b2556 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -20,7 +20,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) internal class ProdApiConfigsManagerTest(private val model: Model) { - private val manager = ProdApiConfigsManager() + private val manager = ProdApiConfigsManager(API_CONFIGS) @Test fun test_getBaseUrl() { @@ -33,9 +33,16 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { private companion object { + val API_CONFIGS = setOf( + Express(), + TangemTech(), + + // Don't forget to add new config + ) + @JvmStatic @Parameterized.Parameters - fun data(): Collection = ApiConfig.values().map { + fun data(): Collection = API_CONFIGS.map { when (it) { is Express -> createExpressModel() is TangemTech -> createTangemTechModel() diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/state/EnvironmentTogglesScreenUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/state/EnvironmentTogglesScreenUM.kt index a11c9dbac0..44a644a9af 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/state/EnvironmentTogglesScreenUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/state/EnvironmentTogglesScreenUM.kt @@ -10,14 +10,12 @@ import kotlinx.collections.immutable.ImmutableSet * @property apiInfoList environment toggles list * @property onEnvironmentSelect the lambda to be invoked when button is pressed * @property onBackClick the lambda to be invoked when back button is pressed - * @property onApplyChangesClick the lambda to be invoked when apply changes button is pressed */ internal data class EnvironmentTogglesScreenUM( @StringRes val title: Int, val apiInfoList: ImmutableSet, val onEnvironmentSelect: (id: String, environment: String) -> Unit, val onBackClick: () -> Unit, - val onApplyChangesClick: () -> Unit, ) { /** diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt index d26480798f..b9c442ce86 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -63,16 +62,6 @@ internal fun EnvironmentTogglesScreen(uiModel: EnvironmentTogglesScreenUM) { ), ) } - - item { - PrimaryButton( - text = stringResource(id = R.string.apply_changes), - onClick = uiModel.onApplyChangesClick, - modifier = Modifier - .fillMaxWidth() - .padding(TangemTheme.dimens.spacing16), - ) - } } } @@ -174,7 +163,6 @@ private fun PreviewFeatureTogglesScreen() { ), onEnvironmentSelect = { _: String, s1: String -> select = s1 }, onBackClick = {}, - onApplyChangesClick = {}, ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt index 84cfee3b6f..24bdf0531b 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt @@ -48,7 +48,7 @@ internal class EnvironmentsTogglesViewModel @Inject constructor( /** Setup navigation state property by router [router] */ fun setupNavigation(router: InnerTesterRouter) { _uiState.update { - it.copy(onBackClick = router::back, onApplyChangesClick = router::back) + it.copy(onBackClick = router::back) } } @@ -85,7 +85,6 @@ internal class EnvironmentsTogglesViewModel @Inject constructor( apiInfoList = persistentSetOf(), onEnvironmentSelect = ::onToggleValueChange, onBackClick = {}, - onApplyChangesClick = {}, ) } From 6851fb8b25f0b08648346f31b81e905fc0e2243a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Aug 2024 20:57:03 +0500 Subject: [PATCH 55/87] Updated on 2026-08-14 --- .../converters/YieldBalanceConverter.kt | 1 + .../staking/model/stakekit/YieldBalance.kt | 1 + .../model/stakekit/YieldBalanceList.kt | 7 ++- .../CurrenciesStatusesLceOperations.kt | 5 ++- .../CurrenciesStatusesOperations.kt | 5 ++- .../tokendetails/TokenDetailsPreviewData.kt | 2 +- .../ui/components/TokenDetailsBalanceBlock.kt | 13 +++--- .../presentation/common/WalletPreviewData.kt | 4 +- .../common/component/TokenItem.kt | 5 ++- .../common/component/token/TokenFiatAmount.kt | 43 +++++++++++++++---- .../common/state/TokenItemState.kt | 5 ++- .../converter/TokenItemStateConverter.kt | 11 +++-- 12 files changed, 75 insertions(+), 27 deletions(-) diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index c46a15f279..b1c9d7d47c 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -24,6 +24,7 @@ internal class YieldBalanceConverter : Converter, ) diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt index 3d3ae126da..01cc23bdc9 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt @@ -5,10 +5,13 @@ sealed class YieldBalanceList { data class Data( val balances: List, ) : YieldBalanceList() { - fun getBalance(rawCurrencyId: String?): YieldBalance { + fun getBalance(rawCurrencyId: String?, networkName: String): YieldBalance { return balances.firstOrNull { yield -> (yield as? YieldBalance.Data)?.balance?.items - ?.any { it.rawCurrencyId == rawCurrencyId } == true + ?.any { + rawCurrencyId == it.rawCurrencyId && + networkName.equals(it.rawNetworkId, ignoreCase = true) + } == true } ?: YieldBalance.Error } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 8dee89d0e6..18f07bea3e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -145,7 +145,10 @@ internal class CurrenciesStatusesLceOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) + val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance( + rawCurrencyId = currency.id.rawCurrencyId, + networkName = currency.network.name, + ) createCurrencyStatus( currency = currency, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 34e8a290cb..e82bad56f2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -261,7 +261,10 @@ internal class CurrenciesStatusesOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) + val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance( + rawCurrencyId = currency.id.rawCurrencyId, + networkName = currency.network.name, + ) createCurrencyStatus( currency = currency, quote = quote, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 182ed49b47..e7625b4013 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -129,7 +129,7 @@ internal object TokenDetailsPreviewData { onBalanceSelect = {}, displayCryptoBalance = "966,96 XLM", displayFiatBalance = "91,50$", - isBalanceSelectorEnabled = false, + isBalanceSelectorEnabled = true, ) val balanceError = TokenDetailsBalanceBlockState.Error( actionButtons = actionButtons, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index d3e4bdff81..b814e9e554 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -147,17 +147,20 @@ private fun BalanceButtons(state: TokenDetailsBalanceBlockState) { .padding(top = TangemTheme.dimens.spacing11) .width(IntrinsicSize.Min), ) { config -> + val style = if (state.selectedBalanceType == config.type) { + TangemTheme.typography.caption1 + } else { + TangemTheme.typography.caption2 + } Text( text = config.title.resolveReference(), color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.caption1, + style = style, maxLines = 1, modifier = Modifier .padding( - start = TangemTheme.dimens.spacing5, - end = TangemTheme.dimens.spacing5, - top = TangemTheme.dimens.spacing3, - bottom = TangemTheme.dimens.spacing3, + horizontal = TangemTheme.dimens.spacing4, + vertical = TangemTheme.dimens.spacing6, ) .align(Alignment.Center), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index e1f6a29f77..0ad7e674a7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -97,7 +97,7 @@ internal object WalletPreviewData { id = UUID.randomUUID().toString(), iconState = coinIconState, titleState = TokenItemState.TitleState.Content(text = "Polygon", hasPending = true), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $"), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = true), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"), cryptoPriceState = TokenItemState.CryptoPriceState.Unknown, onItemClick = {}, @@ -117,7 +117,7 @@ internal object WalletPreviewData { id = UUID.randomUUID().toString(), iconState = tokenIconState, titleState = TokenItemState.TitleState.Content(text = "Polygon"), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $"), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = false), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"), cryptoPriceState = TokenItemState.CryptoPriceState.Content( price = "312 USD", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 01c4e11d48..1917d4e4c8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -403,7 +403,10 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider { FiatAmountText( text = if (isBalanceHidden) StringsSigns.STARS else state.text, - modifier, + hasStaked = state.hasStaked, + modifier = modifier, ) } is TokenFiatAmountState.Loading -> { @@ -32,15 +41,31 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool } @Composable -private fun FiatAmountText(text: String, modifier: Modifier = Modifier) { - Text( - text = text, +private fun FiatAmountText(text: String, hasStaked: Boolean, modifier: Modifier = Modifier) { + Row( modifier = modifier, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = TangemTheme.typography.body2, - ) + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedVisibility( + visible = hasStaked, + ) { + Icon( + painter = rememberVectorPainter(image = ImageVector.vectorResource(R.drawable.ic_staking_24)), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing4) + .size(TangemTheme.dimens.size12), + ) + } + Text( + text = text, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.body2, + ) + } } private fun Modifier.placeholderSize(): Modifier = composed { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index 6338c9f8b0..2ece6053fc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -129,7 +129,10 @@ internal sealed class TokenItemState { @Immutable sealed class FiatAmountState { - data class Content(val text: String) : FiatAmountState() + data class Content( + val text: String, + val hasStaked: Boolean = false, + ) : FiatAmountState() object Loading : FiatAmountState() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt index d7639404cc..9abebae38f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter +import com.tangem.common.extensions.isZero import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter @@ -55,6 +56,7 @@ internal class TokenItemStateConverter( ), fiatAmountState = TokenItemState.FiatAmountState.Content( text = getFormattedFiatAmount(), + hasStaked = !getStakedBalance().isZero(), ), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()), cryptoPriceState = getCryptoPriceState(), @@ -64,21 +66,22 @@ internal class TokenItemStateConverter( } private fun CryptoCurrencyStatus.getFormattedAmount(): String { - val yieldBalance = (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() - val amount = value.amount?.plus(yieldBalance) ?: return DASH_SIGN + val amount = value.amount?.plus(getStakedBalance()) ?: return DASH_SIGN return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) } private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { - val yieldBalance = (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() - val fiatYieldBalance = value.fiatRate?.times(yieldBalance).orZero() + val fiatYieldBalance = value.fiatRate?.times(getStakedBalance()).orZero() val fiatAmount = value.fiatAmount?.plus(fiatYieldBalance) ?: return DASH_SIGN val appCurrency = appCurrencyProvider() return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) } + private fun CryptoCurrencyStatus.getStakedBalance() = + (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() + private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( id = currency.id.value, iconState = iconStateConverter.convert(value = this), From 4b32c119c4fb2e5ef97562ad314f2cee7685753e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Aug 2024 13:27:51 +0500 Subject: [PATCH 56/87] Updated on 2026-08-14 --- .../staking/model/stakekit/YieldBalance.kt | 4 +- .../state/InnerYieldBalanceState.kt | 1 + .../RewardsValidatorStateConverter.kt | 2 +- .../converters/YieldBalancesConverter.kt | 73 ++++++++++--------- .../previewdata/InitialStakingStatePreview.kt | 1 + .../SetInitialDataStateTransformer.kt | 24 ++++-- .../ValidatorSelectChangeTransformer.kt | 12 ++- .../ui/StakingInitialInfoContent.kt | 6 +- .../viewmodel/StakingViewModel.kt | 1 + 9 files changed, 74 insertions(+), 50 deletions(-) diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index 645622ad8f..35072a4cb1 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -11,13 +11,13 @@ sealed class YieldBalance { fun getTotalStakingBalance(): BigDecimal { return balance.items .filterNot { it.type == BalanceType.REWARDS } - .sumOf { it.amount * it.pricePerShare } + .sumOf { it.amount } } fun getRewardStakingBalance(): BigDecimal { return balance.items .filter { it.type == BalanceType.REWARDS } - .sumOf { it.amount * it.pricePerShare } + .sumOf { it.amount } } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index ce7d7eecb5..ef7e4e8fec 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -13,6 +13,7 @@ sealed class InnerYieldBalanceState { val rewardsCrypto: String, val rewardsFiat: String, val isRewardsToClaim: Boolean, + val isRewardsClaimable: Boolean, val balance: ImmutableList, ) : InnerYieldBalanceState() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 107207c36c..a1c35482de 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -44,7 +44,7 @@ internal class RewardsValidatorStateConverter( val validator = yield.validators.firstOrNull { it.address.contains(balance.validatorAddress.orEmpty(), ignoreCase = true) } - val cryptoValue = balance.amount.times(balance.pricePerShare) + val cryptoValue = balance.amount val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue) val unbondingPeriod = yield.metadata.cooldownPeriod.days validator?.toBalanceState( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 85ad364a40..85b64c0645 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.converters +import com.tangem.common.extensions.isZero import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -34,7 +35,9 @@ internal class YieldBalancesConverter( val cryptoRewardsValue = yieldBalance.getRewardStakingBalance() val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue) val groupedBalances = getGroupedBalance(yieldBalance.balance) - + val isRewardsClaimable = yieldBalance.balance.items + .filter { it.type == BalanceType.REWARDS } + .any { it.pendingActions.isNotEmpty() } InnerYieldBalanceState.Data( rewardsCrypto = BigDecimalFormatter.formatCryptoAmount( cryptoAmount = cryptoRewardsValue, @@ -46,6 +49,7 @@ internal class YieldBalancesConverter( fiatCurrencySymbol = appCurrency.symbol, ), isRewardsToClaim = !cryptoRewardsValue.isNullOrZero(), + isRewardsClaimable = isRewardsClaimable, balance = groupedBalances, ) } else { @@ -69,47 +73,50 @@ internal class YieldBalancesConverter( ) } } + .filterNot { it.items.isEmpty() } .toPersistentList() private fun List.mapBalances(): List { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency - return this.mapNotNull { balance -> - val validator = yield.validators.firstOrNull { - balance.validatorAddress?.contains(it.address, ignoreCase = true) == true - } - val cryptoAmount = balance.amount * balance.pricePerShare - val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) - val unbondingPeriod = yield.metadata.cooldownPeriod.days - validator?.let { - BalanceState( - validator = validator, - cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals), - cryptoDecimal = cryptoAmount, - cryptoAmount = stringReference( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoAmount, - cryptoCurrency = cryptoCurrency, + return this + .filterNot { it.amount.isZero() } + .mapNotNull { balance -> + val validator = yield.validators.firstOrNull { + balance.validatorAddress?.contains(it.address, ignoreCase = true) == true + } + val cryptoAmount = balance.amount + val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) + val unbondingPeriod = yield.metadata.cooldownPeriod.days + validator?.let { + BalanceState( + validator = validator, + cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals), + cryptoDecimal = cryptoAmount, + cryptoAmount = stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoAmount, + cryptoCurrency = cryptoCurrency, + ), ), - ), - fiatAmount = stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, + fiatAmount = stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), ), - ), - rawCurrencyId = balance.rawCurrencyId, - unbondingPeriod = pluralReference( - id = R.plurals.common_days, - count = unbondingPeriod, - formatArgs = wrappedList(unbondingPeriod), - ), - pendingActions = balance.pendingActions.toPersistentList(), - ) + rawCurrencyId = balance.rawCurrencyId, + unbondingPeriod = pluralReference( + id = R.plurals.common_days, + count = unbondingPeriod, + formatArgs = wrappedList(unbondingPeriod), + ), + pendingActions = balance.pendingActions.toPersistentList(), + ) + } } - } } private fun BalanceType.toGroup() = when (this) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 6432bbbea7..f31cd5f501 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -60,6 +60,7 @@ internal object InitialStakingStatePreview { rewardsFiat = "100 $", rewardsCrypto = "100 SOL", isRewardsToClaim = false, + isRewardsClaimable = false, balance = persistentListOf( BalanceGroupedState( title = stringReference("Staked"), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index dc3785b501..8d8a69e019 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -13,24 +13,27 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.features.staking.impl.presentation.state.ValidatorState import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter -import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal +@Suppress("LongParameterList") internal class SetInitialDataStateTransformer( private val clickIntents: StakingClickIntents, private val yield: Yield, private val isStakeMoreAvailable: Boolean, + private val isApprovalNeeded: Boolean, private val cryptoCurrencyStatusProvider: Provider, private val userWalletProvider: Provider, private val appCurrencyProvider: Provider, @@ -202,12 +205,17 @@ internal class SetInitialDataStateTransformer( } private fun createInitialConfirmationState(): StakingStates.ConfirmationState { - return ConfirmationStatePreviewData.assentStakingState.copy( - validatorState = ValidatorState.Content( - isClickable = true, - chosenValidator = yield.validators.first(), - availableValidators = yield.validators, - ), + return StakingStates.ConfirmationState.Data( + isPrimaryButtonEnabled = false, + innerState = InnerConfirmationStakingState.ASSENT, + feeState = FeeState.Loading, + validatorState = ValidatorState.Loading, + notifications = persistentListOf(), + footerText = "", + transactionDoneState = TransactionDoneState.Empty, + pendingActions = persistentListOf(), + pendingActionInProgress = null, + isApprovalNeeded = isApprovalNeeded, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt index 273108e5b5..eba4aae409 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -11,13 +11,17 @@ internal class ValidatorSelectChangeTransformer( ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState - val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: return prevState + val validatorState = (confirmationState.validatorState as? ValidatorState.Content)?.copy( + chosenValidator = selectedValidator, + ) ?: ValidatorState.Content( + isClickable = false, + availableValidators = emptyList(), + chosenValidator = selectedValidator, + ) return prevState.copy( confirmationState = confirmationState.copy( - validatorState = validatorState.copy( - chosenValidator = selectedValidator, - ), + validatorState = validatorState, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 810f49be28..4497bf06dc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -87,6 +87,7 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl rewardCrypto = state.yieldBalance.rewardsCrypto, rewardFiat = state.yieldBalance.rewardsFiat, isRewardsToClaim = state.yieldBalance.isRewardsToClaim, + isRewardsClaimable = state.yieldBalance.isRewardsClaimable, onRewardsClick = clickIntents::openRewardsValidators, ) SpacerH12() @@ -140,6 +141,7 @@ private fun StakingRewardBlock( rewardCrypto: String, rewardFiat: String, isRewardsToClaim: Boolean, + isRewardsClaimable: Boolean, onRewardsClick: () -> Unit, ) { val (text, textColor) = if (isRewardsToClaim) { @@ -158,7 +160,7 @@ private fun StakingRewardBlock( InputRowDefault( title = resourceReference(R.string.staking_rewards), text = text, - iconRes = R.drawable.ic_chevron_right_24.takeIf { isRewardsToClaim }, + iconRes = R.drawable.ic_chevron_right_24.takeIf { isRewardsToClaim && isRewardsClaimable }, textColor = textColor, modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) @@ -166,7 +168,7 @@ private fun StakingRewardBlock( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), - enabled = isRewardsToClaim, + enabled = isRewardsToClaim && isRewardsClaimable, onClick = onRewardsClick, ), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index c4c0bac907..a53c3e2bd0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -480,6 +480,7 @@ internal class StakingViewModel @Inject constructor( clickIntents = this@StakingViewModel, yield = yield, isStakeMoreAvailable = isStakeMoreAvailable.getOrElse { false }, + isApprovalNeeded = stakingApproval is StakingApproval.Needed, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider { appCurrency }, From c6d333dbebe75926ea17ab6bb4a3c849e83ce32e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Aug 2024 12:15:55 +0300 Subject: [PATCH 57/87] Updated on 2026-08-14 --- ...tor.kt => SwitchEnvironmentInterceptor.kt} | 25 +++++++---- .../datasource/api/common/config/ApiConfig.kt | 4 +- .../api/common/config/ApiEnvironmentConfig.kt | 18 ++++++++ .../datasource/api/common/config/Express.kt | 42 ++++++++++++++++--- .../api/common/config/TangemTech.kt | 18 ++++++-- .../config/managers/ApiConfigsManager.kt | 5 ++- .../config/managers/DevApiConfigsManager.kt | 5 ++- .../config/managers/ProdApiConfigsManager.kt | 11 ++--- .../tangem/datasource/di/ApiConfigsModule.kt | 9 +++- .../com/tangem/datasource/di/NetworkModule.kt | 14 +++---- .../tangem/datasource/utils/HttpClientExt.kt | 20 +++++++-- .../tangem/datasource/utils/RequestHeader.kt | 26 +++++------- .../managers/ProdApiConfigsManagerTest.kt | 5 ++- .../EnvironmentsTogglesViewModel.kt | 6 +-- 14 files changed, 149 insertions(+), 59 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/api/common/{SwitchBaseUrlInterceptor.kt => SwitchEnvironmentInterceptor.kt} (55%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironmentConfig.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchBaseUrlInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt similarity index 55% rename from core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchBaseUrlInterceptor.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt index 6da54754b1..dbfbc5e54f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchBaseUrlInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt @@ -2,21 +2,23 @@ package com.tangem.datasource.api.common import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.managers.ApiConfigsManager +import com.tangem.utils.Provider import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor +import okhttp3.Request import okhttp3.Response import okio.IOException /** - * Switch base url [Interceptor] + * Switch api environment [Interceptor] * * @property id api config id [ApiConfig.ID] * @property apiConfigsManager api configs manager * [REDACTED_AUTHOR] */ -internal class SwitchBaseUrlInterceptor( +internal class SwitchEnvironmentInterceptor( private val id: ApiConfig.ID, private val apiConfigsManager: ApiConfigsManager, ) : Interceptor { @@ -26,18 +28,27 @@ internal class SwitchBaseUrlInterceptor( var request = chain.request() val builder = request.newBuilder() + val environmentConfig = apiConfigsManager.getEnvironmentConfig(id) + request = builder - .url(url = request.url.adjustBaseUrl()) + .url(url = request.url.adjustBaseUrl(environmentConfig.baseUrl)) + .addHeaders(headers = environmentConfig.headers) .build() return chain.proceed(request) } - private fun HttpUrl.adjustBaseUrl(): HttpUrl { - val host = apiConfigsManager.getBaseUrl(id).toHttpUrl().host - + private fun HttpUrl.adjustBaseUrl(url: String): HttpUrl { return this.newBuilder() - .host(host) + .host(host = url.toHttpUrl().host) .build() } + + private fun Request.Builder.addHeaders(headers: Map>): Request.Builder { + headers.forEach { (name, valueProvider) -> + addHeader(name = name, value = valueProvider()) + } + + return this + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index 693da3fc00..caa3117899 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -14,8 +14,8 @@ sealed class ApiConfig { /** Default environment */ abstract val defaultEnvironment: ApiEnvironment - /** Available environments with base url */ - abstract val environments: Map + /** Available environments */ + abstract val environmentConfigs: List /** Unique id */ val id: ID = initializeId() diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironmentConfig.kt new file mode 100644 index 0000000000..4ab1342caf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironmentConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.api.common.config + +import com.tangem.utils.Provider + +/** + * Api environment config + * + * @property environment environment + * @property baseUrl base url + * @property headers headers + * +[REDACTED_AUTHOR] + */ +data class ApiEnvironmentConfig( + val environment: ApiEnvironment, + val baseUrl: String, + val headers: Map> = emptyMap(), +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt index be5bba3816..d6e5f835bb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -1,18 +1,50 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig +import com.tangem.datasource.utils.RequestHeader +import com.tangem.lib.auth.ExpressAuthProvider +import com.tangem.utils.Provider +import com.tangem.utils.version.AppVersionProvider /** Express [ApiConfig] */ -internal class Express : ApiConfig() { +internal class Express( + private val expressAuthProvider: ExpressAuthProvider, + private val appVersionProvider: AppVersionProvider, +) : ApiConfig() { override val defaultEnvironment: ApiEnvironment = getInitialEnvironment() - override val environments: Map = mapOf( - ApiEnvironment.DEV to "[REDACTED_ENV_URL]", - ApiEnvironment.STAGE to "[REDACTED_ENV_URL]", - ApiEnvironment.PROD to "https://express.tangem.com/v1/", + override val environmentConfigs: List = listOf( + createDevEnvironment(), + createStageEnvironment(), + createProdEnvironment(), ) + private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.DEV, + baseUrl = "[REDACTED_ENV_URL]", + headers = createHeaders(), + ) + + private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.STAGE, + baseUrl = "[REDACTED_ENV_URL]", + headers = createHeaders(), + ) + + private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://express.tangem.com/v1/", + headers = createHeaders(), + ) + + private fun createHeaders() = buildMap { + put(key = "api-key", value = Provider(expressAuthProvider::getApiKey)) + put(key = "user-id", value = Provider(expressAuthProvider::getUserId)) + put(key = "session-id", value = Provider(expressAuthProvider::getSessionId)) + putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values) + } + private companion object { fun getInitialEnvironment(): ApiEnvironment { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt index b1491bb852..f34ffb3590 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt @@ -5,8 +5,20 @@ internal class TangemTech : ApiConfig() { override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD - override val environments: Map = mapOf( - ApiEnvironment.DEV to "https://devapi.tangem-tech.com/v1/", - ApiEnvironment.PROD to "https://api.tangem-tech.com/v1/", + override val environmentConfigs = listOf( + createDevEnvironment(), + createProdEnvironment(), + ) + + private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://api.tangem-tech.com/v1/", + headers = emptyMap(), // TODO: [REDACTED_JIRA] + ) + + private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.DEV, + baseUrl = "https://devapi.tangem-tech.com/v1/", + headers = emptyMap(), // TODO: [REDACTED_JIRA] ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt index 6bb9cbb6ca..880a57f286 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.api.common.config.managers import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironmentConfig /** * Api configs manager @@ -12,6 +13,6 @@ interface ApiConfigsManager { /** Initialize resources */ suspend fun initialize() {} - /** Get base url of api by [id] */ - fun getBaseUrl(id: ApiConfig.ID): String + /** Get environment config by [id] */ + fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt index 4c5f663879..4f4efdd955 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt @@ -3,6 +3,7 @@ package com.tangem.datasource.api.common.config.managers import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.datasource.api.common.config.ApiEnvironmentConfig import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectMap @@ -44,7 +45,7 @@ internal class DevApiConfigsManager( .launchIn(CoroutineScope(dispatchers.main)) } - override fun getBaseUrl(id: ApiConfig.ID): String { + override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig { val apiConfigs = _apiConfigs.value val config = apiConfigs.map { it }.firstOrNull { it.key.id == id }?.key @@ -53,7 +54,7 @@ internal class DevApiConfigsManager( val currentEnvironment = apiConfigs[config] ?: error("Current environment of api config with id [$id] not found") - return config.environments[currentEnvironment] + return config.environmentConfigs.firstOrNull { it.environment == currentEnvironment } ?: error("Api config with id [$id] doesn't contain environment [$currentEnvironment]") } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt index e9785199e6..fd5638c151 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config.managers import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfigs +import com.tangem.datasource.api.common.config.ApiEnvironmentConfig /** * Implementation of [ApiConfigsManager] in PROD environment @@ -12,14 +13,14 @@ internal class ProdApiConfigsManager( private val apiConfigs: ApiConfigs, ) : ApiConfigsManager { - override fun getBaseUrl(id: ApiConfig.ID): String { + override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig { val config = apiConfigs.firstOrNull { it.id == id } - ?: error("Api config with id [$id] not found. Check ApiConfig implementations") + ?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI") - return config.environments[config.defaultEnvironment] + return config.environmentConfigs.firstOrNull { it.environment == config.defaultEnvironment } ?: error( - "Api config with id [$id] doesn't contain " + - "environment [${config.defaultEnvironment}]. Check ApiConfig implementations", + "Api config with id [$id] doesn't contain environment [${config.defaultEnvironment}]. " + + "Check ApiConfig's environments is included default environment", ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index f2bb6573a6..1e3e3d5724 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -3,6 +3,8 @@ package com.tangem.datasource.di import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.Express import com.tangem.datasource.api.common.config.TangemTech +import com.tangem.lib.auth.ExpressAuthProvider +import com.tangem.utils.version.AppVersionProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,7 +17,12 @@ internal object ApiConfigsModule { @Provides @IntoSet - fun provideExpressConfig(): ApiConfig = Express() + fun provideExpressConfig( + expressAuthProvider: ExpressAuthProvider, + appVersionProvider: AppVersionProvider, + ): ApiConfig { + return Express(expressAuthProvider, appVersionProvider) + } @Provides @IntoSet diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index b636e47a19..8ab0db4010 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -21,7 +21,6 @@ import com.tangem.datasource.utils.RequestHeader.* import com.tangem.datasource.utils.addEnvironmentSwitcher import com.tangem.datasource.utils.addHeaders import com.tangem.datasource.utils.addLoggers -import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.version.AppVersionProvider @@ -59,19 +58,18 @@ class NetworkModule { fun provideExpressApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, - expressAuthProvider: ExpressAuthProvider, - appVersionProvider: AppVersionProvider, apiConfigsManager: ApiConfigsManager, ): TangemExpressApi { + val environmentConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.Express) + return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .baseUrl(apiConfigsManager.getBaseUrl(id = ApiConfig.ID.Express)) + .baseUrl(environmentConfig.baseUrl) .client( OkHttpClient.Builder() .addEnvironmentSwitcher(ApiConfig.ID.Express, apiConfigsManager) - .addHeaders(Express(expressAuthProvider)) - .addHeaders(AppVersionPlatformHeaders(appVersionProvider)) + .addHeaders(environmentConfig.headers) .addLoggers(context) .build(), ) @@ -113,7 +111,7 @@ class NetworkModule { context = context, appVersionProvider = appVersionProvider, apiConfigsManager = apiConfigsManager, - baseUrl = apiConfigsManager.getBaseUrl(id = ApiConfig.ID.TangemTech), + baseUrl = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech).baseUrl, ) } @@ -165,7 +163,7 @@ class NetworkModule { context = context, appVersionProvider = appVersionProvider, apiConfigsManager = apiConfigsManager, - baseUrl = apiConfigsManager.getBaseUrl(id = ApiConfig.ID.TangemTech), + baseUrl = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech).baseUrl, timeouts = Timeouts( callTimeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS, ), diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index 0f571dd13a..09387b4279 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -3,10 +3,11 @@ package com.tangem.datasource.utils import android.content.Context import com.chuckerteam.chucker.api.ChuckerInterceptor import com.tangem.datasource.BuildConfig -import com.tangem.datasource.api.common.SwitchBaseUrlInterceptor +import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor +import com.tangem.utils.Provider import okhttp3.Interceptor import okhttp3.OkHttpClient @@ -16,7 +17,7 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade Interceptor { chain -> val request = chain.request().newBuilder().apply { requestHeaders - .flatMap(RequestHeader::values) + .flatMap { it.values.toList() } .forEach { addHeader(it.first, it.second.invoke()) } }.build() @@ -25,6 +26,19 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade ) } +/** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */ +internal fun OkHttpClient.Builder.addHeaders(requestHeaders: Map>): OkHttpClient.Builder { + return addInterceptor( + Interceptor { chain -> + val request = chain.request().newBuilder().apply { + requestHeaders.forEach { addHeader(it.key, it.value.invoke()) } + }.build() + + chain.proceed(request) + }, + ) +} + /** * Extension for logging each [OkHttpClient] request * @@ -53,7 +67,7 @@ internal fun OkHttpClient.Builder.addEnvironmentSwitcher( ): OkHttpClient.Builder { return if (BuildConfig.TESTER_MENU_ENABLED) { addInterceptor( - interceptor = SwitchBaseUrlInterceptor( + interceptor = SwitchEnvironmentInterceptor( id = id, apiConfigsManager = apiConfigsManager, ), diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt index 215b56e9f8..b69481c34c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt @@ -1,8 +1,8 @@ package com.tangem.datasource.utils import com.tangem.datasource.api.common.AuthProvider -import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider +import com.tangem.utils.Provider import com.tangem.utils.version.AppVersionProvider /** @@ -10,31 +10,25 @@ import com.tangem.utils.version.AppVersionProvider * * @param pairs header name and header value pairs */ -sealed class RequestHeader(vararg pairs: Pair String>) { +sealed class RequestHeader(vararg pairs: Pair>) { /** Header list */ - val values: List String>> = pairs.toList() + val values: Map> = pairs.toMap() - data object CacheControlHeader : RequestHeader("Cache-Control" to { "max-age=600" }) + data object CacheControlHeader : RequestHeader("Cache-Control" to Provider { "max-age=600" }) class AuthenticationHeader(authProvider: AuthProvider) : RequestHeader( - "card_id" to { authProvider.getCardId() }, - "card_public_key" to { authProvider.getCardPublicKey() }, - ) - - class Express(expressAuthProvider: ExpressAuthProvider) : RequestHeader( - "api-key" to { expressAuthProvider.getApiKey() }, - "user-id" to { expressAuthProvider.getUserId() }, - "session-id" to { expressAuthProvider.getSessionId() }, + "card_id" to Provider(authProvider::getCardId), + "card_public_key" to Provider(authProvider::getCardPublicKey), ) class AppVersionPlatformHeaders(appVersionProvider: AppVersionProvider) : RequestHeader( - "version" to { appVersionProvider.versionName }, - "platform" to { "android" }, + "version" to Provider(appVersionProvider::versionName), + "platform" to Provider { "android" }, ) class StakeKit(stakeKitAuthProvider: StakeKitAuthProvider) : RequestHeader( - "X-API-KEY" to { stakeKitAuthProvider.getApiKey() }, - "accept" to { "application/json" }, + "X-API-KEY" to Provider(stakeKitAuthProvider::getApiKey), + "accept" to Provider { "application/json" }, ) } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index a2701b2556..a8cdb9a494 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -10,6 +10,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_ import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE import com.tangem.datasource.api.common.config.Express import com.tangem.datasource.api.common.config.TangemTech +import io.mockk.mockk import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -24,7 +25,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { @Test fun test_getBaseUrl() { - val actual = manager.getBaseUrl(id = model.id) + val actual = manager.getEnvironmentConfig(id = model.id).baseUrl Truth.assertThat(actual).isEqualTo(model.expected) } @@ -34,7 +35,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { private companion object { val API_CONFIGS = setOf( - Express(), + Express(mockk(), mockk()), TangemTech(), // Don't forget to add new config diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt index 24bdf0531b..c8bf8c8b56 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt @@ -69,10 +69,10 @@ internal class EnvironmentsTogglesViewModel @Inject constructor( EnvironmentTogglesScreenUM.ApiInfoUM( name = config.id.name, select = currentEnvironment.name, - url = config.environments[currentEnvironment] + url = config.environmentConfigs.firstOrNull { it.environment == currentEnvironment }?.baseUrl ?: error("Current environment's url isn't found"), - environments = config.environments - .map { environment -> environment.key.name } + environments = config.environmentConfigs + .map { environmentConfig -> environmentConfig.environment.name } .toImmutableSet(), ) } From 244f9bc61d684dca5bb5168cd5ee8e39e16171be Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Aug 2024 12:52:27 +0300 Subject: [PATCH 58/87] Updated on 2026-08-14 --- .../tangem/common/ui/charts/MarketChart.kt | 10 +-- .../common/ui/charts/MarketChartMini.kt | 4 ++ .../common/ui/charts/state/MarketChartLook.kt | 1 + .../ui/charts/state/MarketChartState.kt | 1 + .../core/ui/utils/DateTimeFormatters.kt | 61 +++++++++++++++---- .../markets/DefaultMarketsEntryComponent.kt | 36 +++++------ .../impl/model/MarketsTokenDetailsModel.kt | 16 +++++ .../model/converters/InsightsConverter.kt | 22 +++++++ .../impl/model/formatter/Formatters.kt | 13 ++-- .../formatter/MarketsDateTimeFormatters.kt | 2 +- .../details/impl/ui/components/InfoPoint.kt | 60 ++++++++++++++++-- .../impl/ui/components/InsightsBlock.kt | 15 ++--- .../ui/components/MarketTokenDetailsChart.kt | 2 + .../details/impl/ui/state/InfoPointUM.kt | 7 ++- .../details/impl/ui/state/InsightsUM.kt | 1 + .../tokenlist/impl/model/MarketsListModel.kt | 1 + .../statemanager/MarketsListUMStateManager.kt | 22 ++++++- .../components/MarketsListItemPlaceholder.kt | 22 ++----- .../impl/ui/state/MarketsListItemUM.kt | 5 +- 19 files changed, 224 insertions(+), 77 deletions(-) diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt index 2e850fd87d..cd14d8f8c8 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -314,6 +314,7 @@ private fun MarketChartPreview( TangemThemePreview { val growingColor = TangemTheme.colors.icon.accent val fallingColor = TangemTheme.colors.icon.warning + val neutralColor = TangemTheme.colors.icon.informative val chartState = rememberMarketChartState( dataProducer = dataProducer, @@ -324,6 +325,7 @@ private fun MarketChartPreview( when (it) { MarketChartLook.Type.Growing -> growingColor MarketChartLook.Type.Falling -> fallingColor + MarketChartLook.Type.Neutral -> neutralColor } }, ) @@ -382,10 +384,10 @@ private fun MarketChartPreview( dataProducer.runTransaction { updateLook { it.copy( - type = if (it.type == MarketChartLook.Type.Growing) { - MarketChartLook.Type.Falling - } else { - MarketChartLook.Type.Growing + type = when (it.type) { + MarketChartLook.Type.Growing -> MarketChartLook.Type.Falling + MarketChartLook.Type.Falling -> MarketChartLook.Type.Neutral + MarketChartLook.Type.Neutral -> MarketChartLook.Type.Growing }, ) } diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt index 2a124ed8d2..2e62078f0f 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt @@ -36,6 +36,7 @@ fun MarketChartMini( type: MarketChartLook.Type = MarketChartLook.Type.Growing, growingColor: Color = TangemTheme.colors.icon.accent, fallingColor: Color = TangemTheme.colors.icon.warning, + neutralColor: Color = TangemTheme.colors.icon.informative, ) { val model = remember(rawData) { CartesianChartModel(LineCartesianLayerModel.build { series(rawData.y) }) @@ -44,6 +45,7 @@ fun MarketChartMini( val lineColor = when (type) { MarketChartLook.Type.Growing -> growingColor MarketChartLook.Type.Falling -> fallingColor + MarketChartLook.Type.Neutral -> neutralColor } val lineSpec = rememberLine( @@ -85,6 +87,8 @@ private fun Preview() { MarketChartMini(rawData = data, type = MarketChartLook.Type.Growing) SpacerH16() MarketChartMini(rawData = data, type = MarketChartLook.Type.Falling) + SpacerH16() + MarketChartMini(rawData = data, type = MarketChartLook.Type.Neutral) } } } diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt index dc194c3b3b..72c608da53 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt @@ -24,5 +24,6 @@ data class MarketChartLook( enum class Type { Growing, Falling, + Neutral, } } \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt index 8db48deab6..e031fc1a27 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt @@ -24,6 +24,7 @@ fun rememberMarketChartState( when (it) { MarketChartLook.Type.Growing -> Color.Green MarketChartLook.Type.Falling -> Color.Red + MarketChartLook.Type.Neutral -> Color.Gray } } }, 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 20296aec51..4dd1e38fd1 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 @@ -11,12 +11,21 @@ import java.util.Locale object DateTimeFormatters { /** - * Two SS means, SHORT style for date and time. - * If pattern contains "a", it means time is in 12 hour format. - * [Documentation](https://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html) + * Determine if the time is in 12-hour format ("10:00 PM") for the current locale. + */ + val is12HourFormat by lazy { + /** + * Two SS means, SHORT style for date and time. + * If pattern contains "a", it means time is in 12 hour format. + * [Documentation](https://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html) + */ + DateTimeFormat.patternForStyle("SS", Locale.getDefault()).contains("a") + } + + /** + * Example: "12:00 PM", "12:00" */ val timeFormatter: DateTimeFormatter by lazy { - val is12HourFormat = DateTimeFormat.patternForStyle("SS", Locale.getDefault()).contains("a") if (is12HourFormat) { DateTimeFormatterBuilder() .appendClockhourOfHalfday(1) @@ -36,6 +45,9 @@ object DateTimeFormatters { } } + /** + * Example: "1 Jun, 2020", "1 Jun, 2020" + */ val dateFormatter: DateTimeFormatter by lazy { DateTimeFormatterBuilder() .appendDayOfMonth(1) @@ -47,36 +59,61 @@ object DateTimeFormatters { .withLocale(Locale.getDefault()) } + /** + * Example: "31.06.2020", "06/31/2020" + */ val dateDDMMYYYY: DateTimeFormatter by lazy { - DateTimeFormatterBuilder() - .appendPattern("dd.MM.yyyy") - .toFormatter() - .withLocale(Locale.getDefault()) + getBestFormatterBySkeleton("dd.MM.yyyy") } /** - * In API version < 24, there may be some problems with getting the best date and time format pattern. + * Example: "Jun 31, 2020", "31 Jun, 2020" */ val dateMMMdd: DateTimeFormatter by lazy { - getBestFormatterBySkeleton("dd MMM") + getBestFormatterBySkeleton("MMM dd") } + /** + * Example: "2020" + */ val dateYYYY: DateTimeFormatter by lazy { getBestFormatterBySkeleton("yyyy") } + /** + * Example: "31.06.2020 12:00", "06/31/2020 12:00", "06/31/2020 12:00 PM" + */ val dateTimeFormatter: DateTimeFormatter by lazy { - DateTimeFormat.forPattern("dd.MM.yyyy HH:mm") + getBestFormatterBySkeleton("dd.MM.yyyy HH:mm") } fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String { return formatter.print(date) } + /** + * Returns the best date and time format pattern for the given skeleton and the current locale. + * (In API version < 24, there may be some problems with getting the best date and time format pattern.) + * + * @param skeleton The skeleton is an alternative to the pattern. The difference is that the pattern rigidly + * defines the date/time format, while the skeleton specifies only the date/time components (year, month, day, etc.) + * So the order of the components and separators (space, comma, etc.) is not taken into account. + * @see [dateYYYY], [dateMMMdd], [dateDDMMYYYY], [dateTimeFormatter] + */ fun getBestFormatterBySkeleton(skeleton: String): DateTimeFormatter { + val skeletonWithLocale = skeleton.replaceHourLetters() + return DateTimeFormatterBuilder() - .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton)) + .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), skeletonWithLocale)) .toFormatter() .withLocale(Locale.getDefault()) } + + private fun String.replaceHourLetters(): String { + return if (is12HourFormat) { + this.replace('H', 'h').replace('k', 'K') + } else { + this.replace('h', 'H').replace('K', 'k') + } + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt index 0078651a11..a166e55e22 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsEntryComponent.kt @@ -87,8 +87,27 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor( } } + // order of LaunchedEffects is important here + val activeChild = stackState.value.active.configuration + LaunchedEffect(activeChild) { + when (activeChild) { + is MarketsEntryChildFactory.Child.TokenDetails -> { + backgroundColor.animateTo( + secondary, + animationSpec = tween(durationMillis = 500), + ) + } + MarketsEntryChildFactory.Child.TokenList -> { + backgroundColor.animateTo( + primary, + animationSpec = tween(durationMillis = 500), + ) + } + } + } + LaunchedEffect(bottomSheetState.value) { if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) { when (bottomSheetState.value) { @@ -108,23 +127,6 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor( } } - LaunchedEffect(activeChild) { - when (activeChild) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - backgroundColor.animateTo( - secondary, - animationSpec = tween(durationMillis = 500), - ) - } - MarketsEntryChildFactory.Child.TokenList -> { - backgroundColor.animateTo( - primary, - animationSpec = tween(durationMillis = 500), - ) - } - } - } - LaunchedEffect(primary, secondary) { if (backgroundColor.isRunning) return@LaunchedEffect diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 9f9d524a84..2a40b0c83e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -291,6 +291,14 @@ internal class MarketsTokenDetailsModel @Inject constructor( ), ) } + + chartDataProducer.runTransaction { + updateLook { + it.copy( + type = getChartTypeByPercent(percent), + ) + } + } }, ifLeft = { state.update { @@ -347,6 +355,14 @@ internal class MarketsTokenDetailsModel @Inject constructor( dateTimeText = getDefaultDateTimeString(stateToUpdate.selectedInterval), ) } + + chartDataProducer.runTransaction { + updateLook { + it.copy( + type = getChartTypeByPercent(percent), + ) + } + } } private fun onSelectedIntervalChange(interval: PriceChangeInterval) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt index e90f915acd..9bbcff51d9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt @@ -2,6 +2,7 @@ package com.tangem.features.markets.details.impl.model.converters import androidx.compose.runtime.Stable import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketInfo @@ -43,6 +44,14 @@ internal class InsightsConverter( liquidityChange = liquidityChange?.month, buyPressureChange = buyPressureChange?.month, ), + onInfoClick = { + onInfoClick( + InfoBottomSheetContent( + title = resourceReference(R.string.markets_token_details_insights), + body = stringReference("//TODO"), + ), + ) + }, ) } } @@ -57,6 +66,7 @@ internal class InsightsConverter( InfoPointUM( title = resourceReference(R.string.markets_token_details_experienced_buyers), value = experiencedBuyerChange.convertChange(), + change = experiencedBuyerChange.changeType(), onInfoClick = { onInfoClick( InfoBottomSheetContent( @@ -69,6 +79,7 @@ internal class InsightsConverter( InfoPointUM( title = resourceReference(R.string.markets_token_details_buy_pressure), value = buyPressureChange.convertChange(isFiatValue = true), + change = buyPressureChange.changeType(), onInfoClick = { onInfoClick( InfoBottomSheetContent( @@ -81,6 +92,7 @@ internal class InsightsConverter( InfoPointUM( title = resourceReference(R.string.markets_token_details_holders), value = holdersChange.convertChange(), + change = holdersChange.changeType(), onInfoClick = { onInfoClick( InfoBottomSheetContent( @@ -93,6 +105,7 @@ internal class InsightsConverter( InfoPointUM( title = resourceReference(R.string.markets_token_details_liquidity), value = liquidityChange.convertChange(), + change = liquidityChange.changeType(), onInfoClick = { onInfoClick( InfoBottomSheetContent( @@ -105,6 +118,15 @@ internal class InsightsConverter( ) } + private fun BigDecimal?.changeType(): InfoPointUM.ChangeType? { + return when { + this == null -> null + this > BigDecimal.ZERO -> InfoPointUM.ChangeType.UP + this < BigDecimal.ZERO -> InfoPointUM.ChangeType.DOWN + else -> null + } + } + private fun BigDecimal?.convertChange(isFiatValue: Boolean = false): String { if (this == null) return StringsSigns.DASH_SIGN diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt index fd560e41d7..f164289fe6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt @@ -83,16 +83,17 @@ internal fun PriceChangeType.toChartType(): MarketChartLook.Type { return when (this) { PriceChangeType.UP -> MarketChartLook.Type.Growing PriceChangeType.DOWN -> MarketChartLook.Type.Falling - PriceChangeType.NEUTRAL -> MarketChartLook.Type.Growing + PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral } } +@Suppress("MagicNumber") internal fun getChartTypeByPercent(percent: BigDecimal?): MarketChartLook.Type { + val scaled = percent?.setScale(4, RoundingMode.HALF_UP) return when { - percent == null -> return MarketChartLook.Type.Growing - percent == BigDecimal.ZERO -> MarketChartLook.Type.Growing - percent > BigDecimal.ZERO -> MarketChartLook.Type.Growing - percent < BigDecimal.ZERO -> MarketChartLook.Type.Falling - else -> MarketChartLook.Type.Growing + scaled == null -> return MarketChartLook.Type.Neutral + scaled > BigDecimal.ZERO -> MarketChartLook.Type.Growing + scaled < BigDecimal.ZERO -> MarketChartLook.Type.Falling + else -> MarketChartLook.Type.Neutral } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt index e93456eb34..acc0ec44fa 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt @@ -18,7 +18,7 @@ internal object MarketsDateTimeFormatters { private const val WEEK_MILLIS = 7L * H24_MILLIS private val dateTimeMMMFormatter by lazy { - DateTimeFormatters.getBestFormatterBySkeleton("dd MMM hh:mm") + DateTimeFormatters.getBestFormatterBySkeleton("dd MMM Hm") } private val dateFormatter = DateTimeFormatters.dateDDMMYYYY diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt index 09d8317df5..3ca286968f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt @@ -3,13 +3,18 @@ package com.tangem.features.markets.details.impl.ui.components import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerW4 import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.text.TooltipText import com.tangem.core.ui.extensions.resolveReference @@ -40,11 +45,32 @@ internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) overflow = TextOverflow.Ellipsis, ) } - Text( - text = infoPointUM.value, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) + Row { + Text( + text = infoPointUM.value, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + if (infoPointUM.change != null) { + SpacerW4() + Icon( + modifier = Modifier + .size(TangemTheme.dimens.size8) + .align(Alignment.CenterVertically), + imageVector = ImageVector.vectorResource( + id = when (infoPointUM.change) { + InfoPointUM.ChangeType.UP -> R.drawable.ic_arrow_up_8 + InfoPointUM.ChangeType.DOWN -> R.drawable.ic_arrow_down_8 + }, + ), + tint = when (infoPointUM.change) { + InfoPointUM.ChangeType.UP -> TangemTheme.colors.icon.accent + InfoPointUM.ChangeType.DOWN -> TangemTheme.colors.icon.warning + }, + contentDescription = null, + ) + } + } } } @@ -105,6 +131,22 @@ private fun ContentPreview() { onInfoClick = { }, ), ) + InfoPoint( + infoPointUM = InfoPointUM( + title = stringReference("Market Cap"), + value = "$1,000,000", + change = InfoPointUM.ChangeType.UP, + onInfoClick = { }, + ), + ) + InfoPoint( + infoPointUM = InfoPointUM( + title = stringReference("Market Cap"), + value = "$1,000,000", + change = InfoPointUM.ChangeType.DOWN, + onInfoClick = { }, + ), + ) } } } @@ -126,6 +168,14 @@ private fun PreviewShimmer() { modifier = Modifier.fillMaxWidth(), withTooltip = true, ) + InfoPointShimmer( + modifier = Modifier.fillMaxWidth(), + withTooltip = true, + ) + InfoPointShimmer( + modifier = Modifier.fillMaxWidth(), + withTooltip = true, + ) } }, actualContent = { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt index 3cd88bc115..df5dab7cce 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt @@ -7,13 +7,12 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.block.information.GridItems import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.components.text.TooltipText import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme @@ -34,13 +33,10 @@ internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) { InformationBlock( modifier = modifier, title = { - Text( - modifier = Modifier, - text = stringResource(R.string.markets_token_details_insights), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + TooltipText( + text = resourceReference(R.string.markets_token_details_insights), + textStyle = TangemTheme.typography.subtitle2, + onInfoClick = state.onInfoClick, ) }, action = { @@ -180,6 +176,7 @@ private fun ContentPreview() { value = "1 000", ), ), + onInfoClick = {}, ), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt index b19876af06..83bc41555e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt @@ -22,6 +22,7 @@ import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) { val growingColor = TangemTheme.colors.icon.accent val fallingColor = TangemTheme.colors.icon.warning + val neutralColor = TangemTheme.colors.icon.informative val chartState = rememberMarketChartState( dataProducer = state.dataProducer, @@ -29,6 +30,7 @@ internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, mo when (it) { MarketChartLook.Type.Growing -> growingColor MarketChartLook.Type.Falling -> fallingColor + MarketChartLook.Type.Neutral -> neutralColor } }, onMarkerShown = state.onMarkerPointSelected, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt index 5ef1b1ad91..383db4e627 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt @@ -5,5 +5,10 @@ import com.tangem.core.ui.extensions.TextReference internal data class InfoPointUM( val title: TextReference, val value: String, + val change: ChangeType? = null, val onInfoClick: (() -> Unit)? = null, -) \ No newline at end of file +) { + enum class ChangeType { + UP, DOWN + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt index f7d33a8ddc..9156f9bf1b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt @@ -6,4 +6,5 @@ internal data class InsightsUM( val h24Info: ImmutableList, val weekInfo: ImmutableList, val monthInfo: ImmutableList, + val onInfoClick: () -> Unit, ) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index 167b03fd6c..b2d0ca1844 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -48,6 +48,7 @@ internal class MarketsListModel @Inject constructor( private val visibleItemIds = MutableStateFlow>(emptyList()) private val marketsListUMStateManager = MarketsListUMStateManager( + currentVisibleIds = Provider { visibleItemIds.value }, onLoadMoreUiItems = { activeListManager.loadMore() }, visibleItemsChanged = { visibleItemIds.value = it }, onRetryButtonClicked = { activeListManager.reload() }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index 806d5434cb..50170ce417 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -12,6 +12,7 @@ import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM +import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.* @Stable internal class MarketsListUMStateManager( + private val currentVisibleIds: Provider>, private val onLoadMoreUiItems: () -> Unit, private val visibleItemsChanged: (itemsKeys: List) -> Unit, private val onRetryButtonClicked: () -> Unit, @@ -129,9 +131,11 @@ internal class MarketsListUMStateManager( } } + val itemsWithFilteredPriceChange = items.filterPriceChangeByVisibility() + return currentState.copy( list = ListUM.Content( - items = items, + items = itemsWithFilteredPriceChange, loadMore = onLoadMoreUiItems, visibleIdsChanged = visibleItemsChanged, showUnder100kTokens = isInSearchState.not() || isNextPageInSearch, @@ -157,6 +161,22 @@ internal class MarketsListUMStateManager( ) } + // Show price change animation for visible items only + private fun ImmutableList.filterPriceChangeByVisibility(): ImmutableList { + val visibleItemIds = currentVisibleIds() + return map { + it.copy( + price = it.price.copy( + changeType = if (visibleItemIds.contains(it.id)) { + it.price.changeType + } else { + null + }, + ), + ) + }.toImmutableList() + } + private fun generalContentState(newItems: ImmutableList): ListUM.Content { return ListUM.Content( items = newItems, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItemPlaceholder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItemPlaceholder.kt index fa7b373811..692b663cc8 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItemPlaceholder.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListItemPlaceholder.kt @@ -34,11 +34,11 @@ fun MarketsListItemPlaceholder() { SpacerW12() Column(modifier = Modifier.weight(1f)) { - Row( + Box( modifier = Modifier .fillMaxWidth() .padding(vertical = TangemTheme.dimens.spacing4), - horizontalArrangement = Arrangement.SpaceBetween, + contentAlignment = Alignment.CenterStart, ) { RectangleShimmer( modifier = Modifier @@ -46,23 +46,15 @@ fun MarketsListItemPlaceholder() { .height(sp12), radius = TangemTheme.dimens.radius3, ) - SpacerW8() - RectangleShimmer( - modifier = Modifier - .width(TangemTheme.dimens.size70) - .height(sp12), - radius = TangemTheme.dimens.radius3, - ) } SpacerH(height = TangemTheme.dimens.spacing2) - Row( + Box( modifier = Modifier .fillMaxWidth() .padding(vertical = TangemTheme.dimens.spacing2), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom, + contentAlignment = Alignment.CenterStart, ) { RectangleShimmer( modifier = Modifier @@ -70,12 +62,6 @@ fun MarketsListItemPlaceholder() { .height(sp12), radius = TangemTheme.dimens.radius3, ) - RectangleShimmer( - modifier = Modifier - .width(TangemTheme.dimens.size52) - .height(sp12), - radius = TangemTheme.dimens.radius3, - ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt index 020dd5b37a..9f3821c9c5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt @@ -20,10 +20,9 @@ data class MarketsListItemUM( val isUnder100kMarketCap: Boolean = false, ) { val chartType: MarketChartLook.Type = when (trendType) { - PriceChangeType.UP, - PriceChangeType.NEUTRAL, - -> MarketChartLook.Type.Growing + PriceChangeType.UP -> MarketChartLook.Type.Growing PriceChangeType.DOWN -> MarketChartLook.Type.Falling + PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral } @Immutable From 62d0a771d9ace2887b34620459a813a8633b2d70 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Aug 2024 20:00:57 +0300 Subject: [PATCH 59/87] Updated on 2026-08-14 --- .../auth/DefaultExpressAuthProvider.kt | 13 +------- .../tangem/tap/network/auth/di/AuthModule.kt | 10 ++----- .../datasource/api/common/config/Express.kt | 30 +++++++++++++++---- .../datasource/config/ConfigManagerImpl.kt | 4 +-- .../tangem/datasource/config/models/Config.kt | 1 + .../tangem/datasource/di/ApiConfigsModule.kt | 4 ++- .../managers/ProdApiConfigsManagerTest.kt | 2 +- .../tangem/lib/auth/ExpressAuthProvider.kt | 2 -- .../sessionId/ExpressSessionIdGenerator.kt | 6 ---- 9 files changed, 34 insertions(+), 38 deletions(-) delete mode 100644 libs/auth/src/main/java/com/tangem/lib/auth/sessionId/ExpressSessionIdGenerator.kt diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt index f7b18ad5a2..99ddfe6b5b 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt @@ -1,23 +1,16 @@ package com.tangem.tap.network.auth -import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.lib.auth.ExpressAuthProvider -import com.tangem.lib.auth.sessionId.ExpressSessionIdGenerator import java.util.UUID import java.util.concurrent.atomic.AtomicReference internal class DefaultExpressAuthProvider( private val userWalletsStore: UserWalletsStore, - private val configManager: ConfigManager, -) : ExpressAuthProvider, ExpressSessionIdGenerator { +) : ExpressAuthProvider { private var uuid = AtomicReference(UUID.randomUUID()) - override fun getApiKey(): String { - return configManager.config.express?.apiKey ?: error("No express api key provided") - } - override fun getUserId(): String { return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: error("No user id provided") } @@ -25,8 +18,4 @@ internal class DefaultExpressAuthProvider( override fun getSessionId(): String { return uuid.get().toString() } - - override fun generateNewSessionId() { - uuid = AtomicReference(UUID.randomUUID()) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 102598b998..41aa591911 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -29,14 +29,8 @@ class AuthModule { @Provides @Singleton - fun provideExpressAuthProvider( - userWalletsStore: UserWalletsStore, - configManager: ConfigManager, - ): ExpressAuthProvider { - return DefaultExpressAuthProvider( - userWalletsStore = userWalletsStore, - configManager = configManager, - ) + fun provideExpressAuthProvider(userWalletsStore: UserWalletsStore): ExpressAuthProvider { + return DefaultExpressAuthProvider(userWalletsStore = userWalletsStore) } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt index d6e5f835bb..134d24a3ac 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -1,13 +1,21 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig +import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.utils.RequestHeader import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.utils.Provider import com.tangem.utils.version.AppVersionProvider -/** Express [ApiConfig] */ +/** + * Express [ApiConfig] + * + * @property configManager config manager + * @property expressAuthProvider express auth provider + * @property appVersionProvider app version provider + */ internal class Express( + private val configManager: ConfigManager, private val expressAuthProvider: ExpressAuthProvider, private val appVersionProvider: AppVersionProvider, ) : ApiConfig() { @@ -23,28 +31,38 @@ internal class Express( private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.DEV, baseUrl = "[REDACTED_ENV_URL]", - headers = createHeaders(), + headers = createHeaders(isProd = false), ) private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.STAGE, baseUrl = "[REDACTED_ENV_URL]", - headers = createHeaders(), + headers = createHeaders(isProd = false), ) private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, baseUrl = "https://express.tangem.com/v1/", - headers = createHeaders(), + headers = createHeaders(isProd = true), ) - private fun createHeaders() = buildMap { - put(key = "api-key", value = Provider(expressAuthProvider::getApiKey)) + private fun createHeaders(isProd: Boolean) = buildMap { + put(key = "api-key", value = Provider { getApiKey(isProd) }) put(key = "user-id", value = Provider(expressAuthProvider::getUserId)) put(key = "session-id", value = Provider(expressAuthProvider::getSessionId)) putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values) } + private fun getApiKey(isProd: Boolean): String { + return if (isProd) { + configManager.config.express + } else { + configManager.config.devExpress + } + ?.apiKey + ?: error("No express config provided") + } + private companion object { fun getInitialEnvironment(): ApiEnvironment { 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 408493a3d1..af8b2a28c7 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 @@ -1,7 +1,6 @@ package com.tangem.datasource.config import com.tangem.blockchain.common.* -import com.tangem.datasource.BuildConfig import com.tangem.datasource.config.ConfigManager.Companion.IS_CREATING_TWIN_CARDS_ALLOWED import com.tangem.datasource.config.ConfigManager.Companion.IS_TOP_UP_ENABLED import com.tangem.datasource.config.models.Config @@ -109,7 +108,8 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { sprinklr = configValues.sprinklr, walletConnectProjectId = configValues.walletConnectProjectId, tangemComAuthorization = configValues.tangemComAuthorization, - express = if (BuildConfig.ENVIRONMENT == "dev") configValues.devExpress else configValues.express, + express = configValues.express, + devExpress = configValues.devExpress, stakeKitApiKey = configValues.stakeKitApiKey, ) } 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 3f54641502..2b8cadac71 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 @@ -17,5 +17,6 @@ data class Config( val walletConnectProjectId: String = "", val tangemComAuthorization: String? = null, val express: ExpressModel? = null, + val devExpress: ExpressModel? = null, val stakeKitApiKey: String? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index 1e3e3d5724..9959cd7ab7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -3,6 +3,7 @@ package com.tangem.datasource.di import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.Express import com.tangem.datasource.api.common.config.TangemTech +import com.tangem.datasource.config.ConfigManager import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.utils.version.AppVersionProvider import dagger.Module @@ -18,10 +19,11 @@ internal object ApiConfigsModule { @Provides @IntoSet fun provideExpressConfig( + configManager: ConfigManager, expressAuthProvider: ExpressAuthProvider, appVersionProvider: AppVersionProvider, ): ApiConfig { - return Express(expressAuthProvider, appVersionProvider) + return Express(configManager, expressAuthProvider, appVersionProvider) } @Provides diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index a8cdb9a494..135ca3e209 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -35,7 +35,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { private companion object { val API_CONFIGS = setOf( - Express(mockk(), mockk()), + Express(mockk(), mockk(), mockk()), TangemTech(), // Don't forget to add new config diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt b/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt index 4388254787..8582fd21ed 100644 --- a/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt +++ b/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt @@ -2,8 +2,6 @@ package com.tangem.lib.auth interface ExpressAuthProvider { - fun getApiKey(): String - fun getUserId(): String fun getSessionId(): String diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/sessionId/ExpressSessionIdGenerator.kt b/libs/auth/src/main/java/com/tangem/lib/auth/sessionId/ExpressSessionIdGenerator.kt deleted file mode 100644 index be7270b4c7..0000000000 --- a/libs/auth/src/main/java/com/tangem/lib/auth/sessionId/ExpressSessionIdGenerator.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.lib.auth.sessionId - -interface ExpressSessionIdGenerator { - - fun generateNewSessionId() -} \ No newline at end of file From 1d7416c58f020401993aee9ec5f7891299ece3c7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Aug 2024 13:37:27 +0300 Subject: [PATCH 60/87] Updated on 2026-08-14 --- .../api/common/config/TangemTech.kt | 15 ++++- .../api/tangemTech/TangemTechApi.kt | 26 +++++---- .../api/tangemTech/TangemTechServiceApi.kt | 15 ----- .../api/utils/TimeountAnnotations.kt | 39 +++++++++++++ .../tangem/datasource/di/ApiConfigsModule.kt | 2 +- .../com/tangem/datasource/di/NetworkModule.kt | 58 +++++++------------ .../tangem/datasource/utils/HttpClientExt.kt | 32 ++++++++++ .../managers/ProdApiConfigsManagerTest.kt | 2 +- .../manager/ProdFeatureTogglesManagerTest.kt | 10 +--- .../BlockchainProvidersResponseLoader.kt | 4 +- .../BlockchainProvidersResponseLoaderTest.kt | 4 +- 11 files changed, 126 insertions(+), 81 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechServiceApi.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/utils/TimeountAnnotations.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt index f34ffb3590..c93ce9af1e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt @@ -1,7 +1,12 @@ package com.tangem.datasource.api.common.config +import com.tangem.datasource.utils.RequestHeader +import com.tangem.utils.version.AppVersionProvider + /** TangemTech [ApiConfig] */ -internal class TangemTech : ApiConfig() { +internal class TangemTech( + private val appVersionProvider: AppVersionProvider, +) : ApiConfig() { override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD @@ -13,12 +18,16 @@ internal class TangemTech : ApiConfig() { private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, baseUrl = "https://api.tangem-tech.com/v1/", - headers = emptyMap(), // TODO: [REDACTED_JIRA] + headers = createHeaders(), ) private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.DEV, baseUrl = "https://devapi.tangem-tech.com/v1/", - headers = emptyMap(), // TODO: [REDACTED_JIRA] + headers = createHeaders(), ) + + private fun createHeaders() = buildMap { + putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 9fe41bf3a5..23edacd61b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -3,7 +3,10 @@ package com.tangem.datasource.api.tangemTech import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.promotion.models.PromotionInfoResponse import com.tangem.datasource.api.tangemTech.models.* +import com.tangem.datasource.api.utils.ReadTimeout +import com.tangem.datasource.config.models.ProviderModel import retrofit2.http.* +import java.util.concurrent.TimeUnit /** * Interface of Tangem Tech API @@ -15,6 +18,7 @@ interface TangemTechApi { @GET("coins") suspend fun getCoins( + @Header("Cache-Control") cacheControl: String = "max-age=600", @Query("contractAddress") contractAddress: String? = null, @Query("exchangeable") exchangeable: Boolean? = null, @Query("networkIds") networkIds: String? = null, @@ -29,7 +33,9 @@ interface TangemTechApi { suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse @GET("currencies") - suspend fun getCurrencyList(): ApiResponse + suspend fun getCurrencyList( + @Header("Cache-Control") cacheControl: String = "max-age=600", + ): ApiResponse @GET("geo") suspend fun getUserCountryCode(): GeoResponse @@ -59,15 +65,6 @@ interface TangemTechApi { @Body startReferralBody: StartReferralBody, ): ReferralResponse - @GET("shops") - suspend fun getShopInfo(@Query(value = "name") name: String): ShopResponse - - @GET("sales") - suspend fun getSalesInfo( - @Query(value = "locale") locale: String, - @Query(value = "shops") shops: String, - ): SalesResponse - @GET("quotes") suspend fun getQuotes( @Query("currencyId") currencyId: String, @@ -76,7 +73,10 @@ interface TangemTechApi { ): ApiResponse @GET("promotion") - suspend fun getPromotionInfo(@Query("programName") name: String): ApiResponse + suspend fun getPromotionInfo( + @Query("programName") name: String, + @Header("Cache-Control") cacheControl: String = "max-age=600", + ): ApiResponse @GET("settings/{wallet_id}") suspend fun getUserTokensSettings( @@ -131,4 +131,8 @@ interface TangemTechApi { @GET("features") suspend fun getFeatures(): ApiResponse + + @ReadTimeout(duration = 5, unit = TimeUnit.SECONDS) + @GET("networks/providers") + suspend fun getBlockchainProviders(): Map> } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechServiceApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechServiceApi.kt deleted file mode 100644 index 7bd52cc1e2..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechServiceApi.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.datasource.api.tangemTech - -import com.tangem.datasource.config.models.ProviderModel -import retrofit2.http.GET - -/** - * Tangem Tech API for app services - * -[REDACTED_AUTHOR] - */ -interface TangemTechServiceApi { - - @GET("networks/providers") - suspend fun getBlockchainProviders(): Map> -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/utils/TimeountAnnotations.kt b/core/datasource/src/main/java/com/tangem/datasource/api/utils/TimeountAnnotations.kt new file mode 100644 index 0000000000..3e521944cd --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/utils/TimeountAnnotations.kt @@ -0,0 +1,39 @@ +package com.tangem.datasource.api.utils + +import java.util.concurrent.TimeUnit + +/** + * Set connect timeout of request + * + * @property duration duration + * @property unit unit + * + * @see "HttpClientExt.applyTimeoutAnnotations" + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +internal annotation class ConnectTimeout(val duration: Int, val unit: TimeUnit) + +/** + * Set read timeout of request + * + * @property duration duration + * @property unit unit + * + * @see "HttpClientExt.applyTimeoutAnnotations" + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +internal annotation class ReadTimeout(val duration: Int, val unit: TimeUnit) + +/** + * Set write timeout of request + * + * @property duration duration + * @property unit unit + * + * @see "HttpClientExt.applyTimeoutAnnotations" + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +internal annotation class WriteTimeout(val duration: Int, val unit: TimeUnit) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index 9959cd7ab7..f144cc7450 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -28,5 +28,5 @@ internal object ApiConfigsModule { @Provides @IntoSet - fun provideTangemTechConfig(): ApiConfig = TangemTech() + fun provideTangemTechConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemTech(appVersionProvider) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 8ab0db4010..f57577e4a5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -14,13 +14,10 @@ import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApiV2 -import com.tangem.datasource.api.tangemTech.TangemTechServiceApi import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.utils.RequestHeader -import com.tangem.datasource.utils.RequestHeader.* -import com.tangem.datasource.utils.addEnvironmentSwitcher -import com.tangem.datasource.utils.addHeaders -import com.tangem.datasource.utils.addLoggers +import com.tangem.datasource.utils.* +import com.tangem.datasource.utils.RequestHeader.AppVersionPlatformHeaders +import com.tangem.datasource.utils.RequestHeader.StakeKit import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.version.AppVersionProvider @@ -103,16 +100,24 @@ class NetworkModule { fun provideTangemTechApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, - appVersionProvider: AppVersionProvider, apiConfigsManager: ApiConfigsManager, ): TangemTechApi { - return provideTangemTechApiInternal( - moshi = moshi, - context = context, - appVersionProvider = appVersionProvider, - apiConfigsManager = apiConfigsManager, - baseUrl = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech).baseUrl, - ) + val environmentConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech) + + return Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) + .baseUrl(environmentConfig.baseUrl) + .client( + OkHttpClient.Builder() + .addEnvironmentSwitcher(id = ApiConfig.ID.TangemTech, apiConfigsManager = apiConfigsManager) + .addHeaders(environmentConfig.headers) + .applyTimeoutAnnotations() + .addLoggers(context) + .build(), + ) + .build() + .create(TangemTechApi::class.java) } @Provides @@ -150,27 +155,6 @@ class NetworkModule { ) } - @Provides - @Singleton - fun provideTangemTechServiceApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - appVersionProvider: AppVersionProvider, - apiConfigsManager: ApiConfigsManager, - ): TangemTechServiceApi { - return provideTangemTechApiInternal( - moshi = moshi, - context = context, - appVersionProvider = appVersionProvider, - apiConfigsManager = apiConfigsManager, - baseUrl = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech).baseUrl, - timeouts = Timeouts( - callTimeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS, - ), - requestHeaders = listOf(AppVersionPlatformHeaders(appVersionProvider)), - ) - } - @Provides @DevTangemApi @Singleton @@ -202,10 +186,11 @@ class NetworkModule { apiConfigsManager: ApiConfigsManager, baseUrl: String, timeouts: Timeouts = Timeouts(), - requestHeaders: List = listOf(CacheControlHeader, AppVersionPlatformHeaders(appVersionProvider)), + requestHeaders: List = listOf(AppVersionPlatformHeaders(appVersionProvider)), ): T { val client = OkHttpClient.Builder() .addEnvironmentSwitcher(id = ApiConfig.ID.TangemTech, apiConfigsManager = apiConfigsManager) + .applyTimeoutAnnotations() .let { builder -> var b = builder if (timeouts.callTimeoutSeconds != null) { @@ -253,7 +238,6 @@ class NetworkModule { const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/" - const val TANGEM_TECH_SERVICE_TIMEOUT_SECONDS = 5L const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index 09387b4279..f0812aa21c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -7,9 +7,13 @@ import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor +import com.tangem.datasource.api.utils.ConnectTimeout +import com.tangem.datasource.api.utils.ReadTimeout +import com.tangem.datasource.api.utils.WriteTimeout import com.tangem.utils.Provider import okhttp3.Interceptor import okhttp3.OkHttpClient +import retrofit2.Invocation /** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeader): OkHttpClient.Builder { @@ -26,6 +30,34 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade ) } +/** + * Apply timeout annotations [Interceptor]. + * Add this [Interceptor] to [OkHttpClient] if use timeout annotations for retrofit requests. + */ +internal fun OkHttpClient.Builder.applyTimeoutAnnotations(): OkHttpClient.Builder { + return addInterceptor( + Interceptor { chain -> + val request = chain.request() + val tag = request.tag(Invocation::class.java) + val connectionTimeout = tag?.method()?.getAnnotation(ConnectTimeout::class.java) + val readTimeout = tag?.method()?.getAnnotation(ReadTimeout::class.java) + val writeTimeout = tag?.method()?.getAnnotation(WriteTimeout::class.java) + + chain + .apply { + connectionTimeout?.let { withConnectTimeout(timeout = it.duration, unit = it.unit) } + } + .apply { + readTimeout?.let { withReadTimeout(timeout = it.duration, unit = it.unit) } + } + .apply { + writeTimeout?.let { withWriteTimeout(timeout = it.duration, unit = it.unit) } + } + .proceed(request) + }, + ) +} + /** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */ internal fun OkHttpClient.Builder.addHeaders(requestHeaders: Map>): OkHttpClient.Builder { return addInterceptor( diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 135ca3e209..8023906e1c 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -36,7 +36,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { val API_CONFIGS = setOf( Express(mockk(), mockk(), mockk()), - TangemTech(), + TangemTech(mockk()), // Don't forget to add new config ) diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt index a8b90c4af8..35af42ae54 100644 --- a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt @@ -6,21 +6,13 @@ import com.tangem.core.featuretoggle.storage.FeatureToggle import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage import com.tangem.core.featuretoggle.utils.associateToggles import com.tangem.core.featuretoggle.version.VersionProvider -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerifyOrder -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.verifyAll -import kotlinx.coroutines.ExperimentalCoroutinesApi +import io.mockk.* import kotlinx.coroutines.test.runTest import org.junit.Test /** [REDACTED_AUTHOR] */ -@OptIn(ExperimentalCoroutinesApi::class) internal class ProdFeatureTogglesManagerTest { private val localFeatureTogglesStorage = mockk() diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt index bfb8f6107d..d0e43508c0 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt @@ -4,7 +4,7 @@ import androidx.core.util.PatternsCompat import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchainsdk.BlockchainProvidersResponse import com.tangem.blockchainsdk.utils.createPrivateProviderType -import com.tangem.datasource.api.tangemTech.TangemTechServiceApi +import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.config.models.ProviderModel import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -24,7 +24,7 @@ import javax.inject.Singleton */ @Singleton internal class BlockchainProvidersResponseLoader @Inject constructor( - private val tangemTechServiceApi: TangemTechServiceApi, + private val tangemTechServiceApi: TangemTechApi, private val assetLoader: AssetLoader, private val dispatchers: CoroutineDispatcherProvider, ) { diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoaderTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoaderTest.kt index afbf3601ee..142425d558 100644 --- a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoaderTest.kt +++ b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoaderTest.kt @@ -7,7 +7,7 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import com.tangem.blockchainsdk.BlockchainProvidersResponse -import com.tangem.datasource.api.tangemTech.TangemTechServiceApi +import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.asset.reader.AssetReader import com.tangem.datasource.config.models.ProviderModel @@ -24,7 +24,7 @@ import org.junit.Test @OptIn(ExperimentalStdlibApi::class) internal class BlockchainProvidersResponseLoaderTest { - private val tangemTechServiceApi = mockk() + private val tangemTechServiceApi = mockk() private val assetReader = mockk() private val moshi = mockk() private val jsonAdapter = mockk>() From ad3e6eb3228241c0e8565d12b5ac9f4c5fa61520 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Aug 2024 16:17:28 +0300 Subject: [PATCH 61/87] Updated on 2026-08-14 --- .../src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index c9be6557a7..58b9388a8e 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -359,5 +359,4 @@ private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Nexa, Blockchain.NexaTestnet, - Blockchain.Filecoin, ) \ No newline at end of file From 5bd5e63f182f32a21afac4011ed219e8594b7a6f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Aug 2024 18:23:39 +0500 Subject: [PATCH 62/87] Updated on 2026-08-14 --- .../components/inputrow/InputRowImageBase.kt | 79 +- .../inputrow/InputRowImageChevron.kt | 82 -- .../components/inputrow/InputRowImageInfo.kt | 110 ++- .../inputrow/InputRowImageSelector.kt | 7 +- .../core/ui/decorations/RoundedDecorations.kt | 14 +- .../main/res/drawable/ic_chevron_right_24.xml | 13 +- .../main/res/drawable/img_staking_banner.xml | 698 +++++++++--------- .../SetButtonsStateTransformer.kt | 2 +- .../ui/StakingClaimRewardsValidatorContent.kt | 26 +- .../ui/StakingInitialInfoContent.kt | 77 +- .../ui/StakingValidatorListContent.kt | 66 +- .../presentation/ui/block/StakingFeeBlock.kt | 2 +- .../presentation/ui/block/ValidatorBlock.kt | 48 +- 13 files changed, 640 insertions(+), 584 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt index 9af3516af2..0442dc06ce 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt @@ -1,39 +1,68 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +/** + * Input row component with selector + * [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2841-1589&t=u6pOF6lsdpvWLELb-4) + * + * @param subtitle subtitle text + * @param caption caption text + * @param modifier modifier + * @param imageUrl icon to load + * @param subtitleColor subtitle text color + * @param captionColor caption text color + * @param isGrayscaleImage whether to display grayscale image + * @param iconEndRes icon to end of row + * @param extraContent extra content + */ @Composable internal fun InputRowImageBase( subtitle: TextReference, - caption: TextReference?, imageUrl: String, modifier: Modifier = Modifier, + caption: TextReference? = null, subtitleColor: Color = TangemTheme.colors.text.primary1, captionColor: Color = TangemTheme.colors.text.tertiary, isGrayscaleImage: Boolean = false, - extraContent: @Composable RowScope.() -> Unit = {}, + iconEndRes: Int? = null, + extraContent: (@Composable RowScope.() -> Unit)? = null, ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = modifier, ) { InputRowAsyncImage( imageUrl = imageUrl, isGrayscale = isGrayscaleImage, modifier = Modifier - .size(TangemTheme.dimens.spacing36), + .size(TangemTheme.dimens.spacing36) + .clip(TangemTheme.shapes.roundedCornersXLarge), ) + SpacerW12() Column { Text( text = subtitle.resolveReference(), @@ -49,6 +78,44 @@ internal fun InputRowImageBase( ) } } - extraContent() + SpacerW12() + if (extraContent != null) { + extraContent() + } else { + SpacerWMax() + } + InputRowEndIcon(iconEndRes) } -} \ No newline at end of file +} + +@Composable +private fun RowScope.InputRowEndIcon(iconRes: Int?) { + AnimatedVisibility( + visible = iconRes != null, + label = "Icon visibility animation", + ) { + val icon = remember(this) { requireNotNull(iconRes) } + Icon( + painter = rememberVectorPainter(image = ImageVector.vectorResource(id = icon)), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing6), + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun InputRowImageBase_Preview() { + TangemThemePreview { + InputRowImageBase( + subtitle = TextReference.Str("Binance"), + caption = TextReference.Str("APR 3,54%"), + imageUrl = "", + iconEndRes = R.drawable.ic_chevron_right_24, + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt deleted file mode 100644 index c341f3ceac..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.core.ui.components.inputrow - -import android.content.res.Configuration -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.R -import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -/** - * Input row component with selector - * [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2100-842&t=hoBXmDX8NeLrp4p6-4) - * - * @param subtitle subtitle text - * @param caption caption text - * @param imageUrl icon to load - * @param modifier modifier - * @param subtitleColor subtitle text color - * @param captionColor caption text color - */ -@Composable -fun InputRowImageChevron( - subtitle: TextReference, - caption: TextReference, - imageUrl: String, - modifier: Modifier = Modifier, - subtitleColor: Color = TangemTheme.colors.text.primary1, - captionColor: Color = TangemTheme.colors.text.tertiary, - showChevron: Boolean = true, -) { - InputRowImageBase( - subtitle = subtitle, - caption = caption, - imageUrl = imageUrl, - modifier = modifier, - subtitleColor = subtitleColor, - captionColor = captionColor, - ) { - SpacerWMax() - if (showChevron) { - Icon( - painter = painterResource(id = R.drawable.ic_chevron_right_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } - } -} - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun InputRowImageChevron_Preview() { - TangemThemePreview { - InputRowImageChevron( - subtitle = stringReference("Binance"), - caption = combinedReference( - resourceReference(R.string.staking_details_apr), - annotatedReference( - buildAnnotatedString { - append(" ") - withStyle(SpanStyle(TangemTheme.colors.text.accent)) { - stringReference("3,54%") - } - }, - ), - ), - imageUrl = "", - ) - } -} -// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt index f9811bbe1a..967547e085 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt @@ -9,10 +9,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle 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.R import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.extensions.* @@ -33,20 +33,22 @@ import com.tangem.core.ui.res.TangemThemePreview * @param subtitleColor subtitle text color * @param captionColor caption text color * @param isGrayscaleImage whether to display grayscale image + * @param iconEndRes icon to end of row */ @Suppress("LongParameterList") @Composable fun InputRowImageInfo( subtitle: TextReference, - caption: TextReference, infoTitle: TextReference, - infoSubtitle: TextReference, imageUrl: String, modifier: Modifier = Modifier, title: TextReference? = null, + caption: TextReference? = null, + infoSubtitle: TextReference? = null, subtitleColor: Color = TangemTheme.colors.text.primary1, captionColor: Color = TangemTheme.colors.text.tertiary, isGrayscaleImage: Boolean = false, + iconEndRes: Int? = null, ) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), @@ -67,22 +69,41 @@ fun InputRowImageInfo( subtitleColor = subtitleColor, captionColor = captionColor, isGrayscaleImage = isGrayscaleImage, + iconEndRes = iconEndRes, ) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), horizontalAlignment = Alignment.End, modifier = Modifier.weight(1f), ) { - EllipsisText( - text = infoTitle.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - EllipsisText( - text = infoSubtitle.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) + if (infoTitle is TextReference.Annotated) { + Text( + text = infoTitle.resolveAnnotatedReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } else { + EllipsisText( + text = infoTitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } + infoSubtitle?.let { + if (infoSubtitle is TextReference.Annotated) { + Text( + text = infoSubtitle.resolveAnnotatedReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } else { + EllipsisText( + text = infoSubtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } } } } @@ -92,26 +113,55 @@ fun InputRowImageInfo( @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowImageInfo_Preview() { +private fun InputRowImageInfo_Preview( + @PreviewParameter(InputRowImageInfoPreviewDataProvider::class) data: InputRowImageInfoPreviewData, +) { TangemThemePreview { InputRowImageInfo( - title = stringReference("Active"), - subtitle = stringReference("Binance"), - caption = combinedReference( - resourceReference(R.string.staking_details_apr), - annotatedReference( - buildAnnotatedString { - append(" ") - withStyle(SpanStyle(TangemTheme.colors.text.accent)) { - stringReference("3,54%") - } - }, - ), - ), - infoTitle = stringReference("5431231231231231231231232 USD"), - infoSubtitle = stringReference("5 SOL"), + title = data.title, + subtitle = data.subtitle, + caption = data.caption, + infoTitle = data.infoTitle, + infoSubtitle = data.infoSubtitle, imageUrl = "", + iconEndRes = R.drawable.ic_chevron_right_24, ) } } + +private class InputRowImageInfoPreviewDataProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InputRowImageInfoPreviewData( + title = stringReference("Validator"), + subtitle = stringReference("Binance"), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + append(" ") + append("3,54%") + }, + ), + ), + infoTitle = stringReference("5431231231231231231231232 USD"), + infoSubtitle = stringReference("5 SOL"), + ), + InputRowImageInfoPreviewData( + title = null, + subtitle = stringReference("Binance"), + caption = null, + infoTitle = stringReference("5431231231231231231231232 USD"), + infoSubtitle = null, + ), + ) +} + +private data class InputRowImageInfoPreviewData( + val title: TextReference?, + val subtitle: TextReference, + val caption: TextReference?, + val infoTitle: TextReference, + val infoSubtitle: TextReference?, +) // endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt index 5231d7f896..e1aa081b50 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt @@ -40,6 +40,7 @@ import java.math.BigDecimal * @param subtitleColor subtitle text color * @param captionColor caption text color * @param isSelected true if selected + * @param selectorContent selector content */ @Composable fun InputRowImageSelector( @@ -51,6 +52,7 @@ fun InputRowImageSelector( subtitleColor: Color = TangemTheme.colors.text.primary1, captionColor: Color = TangemTheme.colors.text.tertiary, isSelected: Boolean = false, + selectorContent: @Composable ((isSelected: Boolean, isEnabled: Boolean, onSelect: () -> Unit) -> Unit), ) { InputRowImageBase( subtitle = subtitle, @@ -67,7 +69,7 @@ fun InputRowImageSelector( .padding(TangemTheme.dimens.spacing12), ) { SpacerWMax() - TangemRadioButton(isSelected = isSelected, isEnabled = false, onClick = onSelect) + selectorContent(isSelected, false, onSelect) } } @@ -98,6 +100,9 @@ private fun InputRowImageSelectorPreview( imageUrl = "", isSelected = false, onSelect = {}, + selectorContent = { isSelected, isEnabled, onSelect -> + TangemRadioButton(isSelected = isSelected, isEnabled = isEnabled, onClick = onSelect) + }, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt index 78a23e7b11..dabc6f97e2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt @@ -2,15 +2,19 @@ package com.tangem.core.ui.decorations import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp import com.tangem.core.ui.res.TangemTheme +@Composable fun Modifier.roundedShapeItemDecoration( currentIndex: Int, lastIndex: Int, addDefaultPadding: Boolean = true, + radius: Dp = TangemTheme.dimens.radius16, ): Modifier = composed { val modifier = if (addDefaultPadding) this.padding(horizontal = TangemTheme.dimens.spacing16) else this val isSingleItem = currentIndex == 0 && lastIndex == 0 @@ -24,7 +28,7 @@ fun Modifier.roundedShapeItemDecoration( Modifier }, ) - .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .clip(shape = RoundedCornerShape(radius)) } currentIndex == 0 -> { modifier @@ -37,8 +41,8 @@ fun Modifier.roundedShapeItemDecoration( ) .clip( shape = RoundedCornerShape( - topStart = TangemTheme.dimens.radius16, - topEnd = TangemTheme.dimens.radius16, + topStart = radius, + topEnd = radius, ), ) } @@ -46,8 +50,8 @@ fun Modifier.roundedShapeItemDecoration( modifier .clip( shape = RoundedCornerShape( - bottomStart = TangemTheme.dimens.radius16, - bottomEnd = TangemTheme.dimens.radius16, + bottomStart = radius, + bottomEnd = radius, ), ) } diff --git a/core/ui/src/main/res/drawable/ic_chevron_right_24.xml b/core/ui/src/main/res/drawable/ic_chevron_right_24.xml index 1bb30766a3..963e79f50b 100644 --- a/core/ui/src/main/res/drawable/ic_chevron_right_24.xml +++ b/core/ui/src/main/res/drawable/ic_chevron_right_24.xml @@ -1,10 +1,5 @@ - - + + + + diff --git a/core/ui/src/main/res/drawable/img_staking_banner.xml b/core/ui/src/main/res/drawable/img_staking_banner.xml index b3dfcacad4..61787d856c 100644 --- a/core/ui/src/main/res/drawable/img_staking_banner.xml +++ b/core/ui/src/main/res/drawable/img_staking_banner.xml @@ -1,345 +1,359 @@ - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index c954aee97d..6b4515a6ba 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -131,7 +131,7 @@ internal class SetButtonsStateTransformer : Transformer { if (initialState?.yieldBalance is InnerYieldBalanceState.Data) { resourceReference(R.string.staking_stake_more) } else { - resourceReference(R.string.common_next) + resourceReference(R.string.common_stake) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt index 49d711da2d..3e6723c8b2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt @@ -9,9 +9,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* @@ -41,19 +38,16 @@ internal fun StakingClaimRewardsValidatorContent( subtitle = stringReference(item.validator.name), caption = combinedReference( resourceReference(R.string.staking_details_apr), - annotatedReference( - buildAnnotatedString { - appendSpace() - withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { - append( - BigDecimalFormatter.formatPercent( - percent = item.validator.apr.orZero(), - useAbsoluteValue = true, - ), - ) - } - }, - ), + annotatedReference { + appendSpace() + appendColored( + text = BigDecimalFormatter.formatPercent( + percent = item.validator.apr.orZero(), + useAbsoluteValue = true, + ), + color = TangemTheme.colors.text.accent, + ) + }, ), infoTitle = item.fiatAmount, infoSubtitle = item.cryptoAmount, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 810f49be28..02a540a60c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -16,7 +17,8 @@ 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.* +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -109,6 +111,7 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl private fun BannerBlock(onClick: () -> Unit) { Box( modifier = Modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), @@ -189,42 +192,26 @@ private fun ActiveStakingBlock(groups: ImmutableList, onCli .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), ) { - group.items.forEachIndexed { index, balance -> + Text( + text = group.title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding( + top = TangemTheme.dimens.spacing12, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + ) + group.items.forEach { balance -> key(balance.validator.address) { - val caption = if (group.type == BalanceType.UNSTAKING) { - combinedReference( - resourceReference(R.string.staking_details_unbonding_period), - annotatedReference { - appendSpace() - appendColored( - text = balance.unbondingPeriod.resolveReference(), - color = TangemTheme.colors.text.accent, - ) - }, - ) - } else { - combinedReference( - resourceReference(R.string.app_name), - annotatedReference { - appendSpace() - appendColored( - text = BigDecimalFormatter.formatPercent( - percent = balance.validator.apr.orZero(), - useAbsoluteValue = true, - ), - color = TangemTheme.colors.text.accent, - ) - }, - ) - } InputRowImageInfo( - title = group.title.takeIf { index == 0 }, subtitle = stringReference(balance.validator.name), - caption = caption, + caption = getCaption(group.type, balance), isGrayscaleImage = group.type == BalanceType.UNSTAKING, infoTitle = balance.fiatAmount, infoSubtitle = balance.cryptoAmount, imageUrl = balance.validator.image.orEmpty(), + iconEndRes = R.drawable.ic_chevron_right_24.takeIf { group.isClickable }, modifier = Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), @@ -241,6 +228,36 @@ private fun ActiveStakingBlock(groups: ImmutableList, onCli } } +@Composable +private fun getCaption(balanceType: BalanceType, balance: BalanceState): TextReference { + return if (balanceType == BalanceType.UNSTAKING) { + combinedReference( + resourceReference(R.string.staking_details_unbonding_period), + annotatedReference { + appendSpace() + appendColored( + text = balance.unbondingPeriod.resolveReference(), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } else { + combinedReference( + resourceReference(R.string.app_name), + annotatedReference { + appendSpace() + appendColored( + text = BigDecimalFormatter.formatPercent( + percent = balance.validator.apr.orZero(), + useAbsoluteValue = true, + ), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } +} + private val textGradientColors = listOf( TangemColorPalette.White, Color(0xff8fb4df), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt index fe98e24c83..e6ed596800 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -1,25 +1,26 @@ package com.tangem.features.staking.impl.presentation.ui import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle +import androidx.compose.ui.res.vectorResource 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.inputrow.InputRowImageSelector -import com.tangem.core.ui.components.rows.CornersToRound -import com.tangem.core.ui.extensions.annotatedReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter @@ -28,7 +29,7 @@ import com.tangem.features.staking.impl.presentation.state.ValidatorState import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents -import java.math.BigDecimal +import com.tangem.utils.extensions.orZero /** * Staking screen with validators @@ -58,29 +59,40 @@ internal fun StakingValidatorListContent( subtitle = stringReference(item.name), caption = combinedReference( resourceReference(R.string.staking_details_apr), - annotatedReference( - buildAnnotatedString { - append(" ") - withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) { - append( - BigDecimalFormatter.formatPercent(item.apr ?: BigDecimal.ZERO, true), - ) - } - }, - ), + annotatedReference { + appendSpace() + appendColored( + text = BigDecimalFormatter.formatPercent(item.apr.orZero(), true), + color = TangemTheme.colors.text.accent, + ) + }, ), imageUrl = item.image.orEmpty(), isSelected = item == state.chosenValidator, onSelect = { clickIntents.onValidatorSelect(item) }, modifier = Modifier - .clip( - when (index) { - 0 -> CornersToRound.TOP_2 - validators.lastIndex -> CornersToRound.BOTTOM_2 - else -> CornersToRound.ZERO - }.getShape(), + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = validators.lastIndex, + radius = TangemTheme.dimens.radius12, + addDefaultPadding = + false, ) .background(TangemTheme.colors.background.action), + selectorContent = { checked, _, _ -> + AnimatedVisibility( + modifier = Modifier, + visible = checked, + ) { + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(id = R.drawable.ic_check_24), + ), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + }, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index e710b6b0b7..fdca1be81a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -40,7 +40,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { Text( text = stringResource(R.string.common_network_fee_title), style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, + color = TangemTheme.colors.text.tertiary, ) Box( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt index ffd1d0c966..abb57b51a7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -5,27 +5,18 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material.ripple.rememberRipple -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.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle -import com.tangem.core.ui.components.inputrow.InputRowImageChevron -import com.tangem.core.ui.extensions.annotatedReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.components.inputrow.InputRowImageInfo +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.ValidatorState -import java.math.BigDecimal +import com.tangem.utils.extensions.orZero @Composable internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit) { @@ -39,32 +30,21 @@ internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit) interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), onClick = onClick, - ) - .padding(TangemTheme.dimens.spacing12), + ), ) { - Text( - text = stringResource(R.string.staking_validator), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6), - ) if (validatorState is ValidatorState.Content) { - InputRowImageChevron( + InputRowImageInfo( + title = resourceReference(R.string.staking_validator), subtitle = stringReference(validatorState.chosenValidator.name), - caption = combinedReference( - resourceReference(R.string.staking_details_apr), - annotatedReference( - buildAnnotatedString { - append(" ") - withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { - val apr = validatorState.chosenValidator.apr ?: BigDecimal.ZERO - append(BigDecimalFormatter.formatPercent(apr, true)) - } - }, - ), - ), + infoTitle = annotatedReference { + append(resourceReference(R.string.staking_details_apr).resolveReference()) + appendSpace() + appendColored( + text = BigDecimalFormatter.formatPercent(validatorState.chosenValidator.apr.orZero(), true), + color = TangemTheme.colors.text.accent, + ) + }, imageUrl = validatorState.chosenValidator.image.orEmpty(), - showChevron = validatorState.isClickable, ) } } From cba839d8f24f1a6df6ad4d88700eb865e3c8c256 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Aug 2024 15:47:19 +0300 Subject: [PATCH 63/87] Updated on 2026-08-14 --- .../datasource/api/common/config/ApiConfig.kt | 2 ++ .../datasource/api/common/config/StakeKit.kt | 33 +++++++++++++++++++ .../tangem/datasource/di/ApiConfigsModule.kt | 8 +++++ .../com/tangem/datasource/di/NetworkModule.kt | 20 +++++------ .../tangem/datasource/utils/HttpClientExt.kt | 8 +++-- .../tangem/datasource/utils/RequestHeader.kt | 6 ---- .../managers/ProdApiConfigsManagerTest.kt | 10 ++++++ .../EnvironmentsTogglesViewModel.kt | 4 ++- 8 files changed, 69 insertions(+), 22 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index caa3117899..65bc7a1e66 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -23,12 +23,14 @@ sealed class ApiConfig { enum class ID { Express, TangemTech, + StakeKit, } private fun initializeId(): ID { return when (this) { is Express -> ID.Express is TangemTech -> ID.TangemTech + is StakeKit -> ID.StakeKit } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt new file mode 100644 index 0000000000..4370c06c92 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt @@ -0,0 +1,33 @@ +package com.tangem.datasource.api.common.config + +import com.tangem.lib.auth.StakeKitAuthProvider +import com.tangem.utils.Provider + +/** + * StakeKit [ApiConfig] + * + * @property stakeKitAuthProvider StakeKit auth provider + * +[REDACTED_AUTHOR] + */ +internal class StakeKit( + private val stakeKitAuthProvider: StakeKitAuthProvider, +) : ApiConfig() { + + override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD + + override val environmentConfigs: List = listOf( + createProdEnvironment(), + ) + + private fun createProdEnvironment(): ApiEnvironmentConfig { + return ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://api.stakek.it/v1/", + headers = mapOf( + "X-API-KEY" to Provider(stakeKitAuthProvider::getApiKey), + "accept" to Provider { "application/json" }, + ), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index f144cc7450..1dd7fb700c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -2,9 +2,11 @@ package com.tangem.datasource.di import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.Express +import com.tangem.datasource.api.common.config.StakeKit import com.tangem.datasource.api.common.config.TangemTech import com.tangem.datasource.config.ConfigManager import com.tangem.lib.auth.ExpressAuthProvider +import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.utils.version.AppVersionProvider import dagger.Module import dagger.Provides @@ -26,6 +28,12 @@ internal object ApiConfigsModule { return Express(configManager, expressAuthProvider, appVersionProvider) } + @Provides + @IntoSet + fun provideStakeKitConfig(stakeKitAuthProvider: StakeKitAuthProvider): ApiConfig { + return StakeKit(stakeKitAuthProvider) + } + @Provides @IntoSet fun provideTangemTechConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemTech(appVersionProvider) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index f57577e4a5..a393f14ee7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -17,8 +17,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApiV2 import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.utils.* import com.tangem.datasource.utils.RequestHeader.AppVersionPlatformHeaders -import com.tangem.datasource.utils.RequestHeader.StakeKit -import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.version.AppVersionProvider import dagger.Module @@ -65,8 +63,7 @@ class NetworkModule { .baseUrl(environmentConfig.baseUrl) .client( OkHttpClient.Builder() - .addEnvironmentSwitcher(ApiConfig.ID.Express, apiConfigsManager) - .addHeaders(environmentConfig.headers) + .applyApiConfig(ApiConfig.ID.Express, apiConfigsManager) .addLoggers(context) .build(), ) @@ -79,15 +76,17 @@ class NetworkModule { fun provideStakeKitApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, - stakeKitAuthProvider: StakeKitAuthProvider, + apiConfigsManager: ApiConfigsManager, ): StakeKitApi { + val environmentConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.StakeKit) + return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .baseUrl(STAKEKIT_BASE_URL) + .baseUrl(environmentConfig.baseUrl) .client( OkHttpClient.Builder() - .addHeaders(StakeKit(stakeKitAuthProvider)) + .applyApiConfig(id = ApiConfig.ID.StakeKit, apiConfigsManager = apiConfigsManager) .addLoggers(context) .build(), ) @@ -110,8 +109,7 @@ class NetworkModule { .baseUrl(environmentConfig.baseUrl) .client( OkHttpClient.Builder() - .addEnvironmentSwitcher(id = ApiConfig.ID.TangemTech, apiConfigsManager = apiConfigsManager) - .addHeaders(environmentConfig.headers) + .applyApiConfig(id = ApiConfig.ID.TangemTech, apiConfigsManager = apiConfigsManager) .applyTimeoutAnnotations() .addLoggers(context) .build(), @@ -189,7 +187,7 @@ class NetworkModule { requestHeaders: List = listOf(AppVersionPlatformHeaders(appVersionProvider)), ): T { val client = OkHttpClient.Builder() - .addEnvironmentSwitcher(id = ApiConfig.ID.TangemTech, apiConfigsManager = apiConfigsManager) + .applyApiConfig(id = ApiConfig.ID.TangemTech, apiConfigsManager = apiConfigsManager) .applyTimeoutAnnotations() .let { builder -> var b = builder @@ -232,8 +230,6 @@ class NetworkModule { ) private companion object { - const val STAKEKIT_BASE_URL = "https://api.stakek.it/v1/" - const val DEV_V1_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/" const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/" diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index f0812aa21c..bd6e251c2b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -88,12 +88,12 @@ internal fun OkHttpClient.Builder.addLoggers(context: Context? = null): OkHttpCl } /** - * Add environment switcher + * Apply api config * * @param id class of [ApiConfig] * @param apiConfigsManager api configs manager */ -internal fun OkHttpClient.Builder.addEnvironmentSwitcher( +internal fun OkHttpClient.Builder.applyApiConfig( id: ApiConfig.ID, apiConfigsManager: ApiConfigsManager, ): OkHttpClient.Builder { @@ -105,6 +105,8 @@ internal fun OkHttpClient.Builder.addEnvironmentSwitcher( ), ) } else { - this + val headers = apiConfigsManager.getEnvironmentConfig(id).headers + + this.addHeaders(headers) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt index b69481c34c..b6ed8b7645 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt @@ -1,7 +1,6 @@ package com.tangem.datasource.utils import com.tangem.datasource.api.common.AuthProvider -import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.utils.Provider import com.tangem.utils.version.AppVersionProvider @@ -26,9 +25,4 @@ sealed class RequestHeader(vararg pairs: Pair>) { "version" to Provider(appVersionProvider::versionName), "platform" to Provider { "android" }, ) - - class StakeKit(stakeKitAuthProvider: StakeKitAuthProvider) : RequestHeader( - "X-API-KEY" to Provider(stakeKitAuthProvider::getApiKey), - "accept" to Provider { "application/json" }, - ) } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 8023906e1c..f5b88829a3 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -9,6 +9,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUIL import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE import com.tangem.datasource.api.common.config.Express +import com.tangem.datasource.api.common.config.StakeKit import com.tangem.datasource.api.common.config.TangemTech import io.mockk.mockk import org.junit.Test @@ -37,6 +38,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { val API_CONFIGS = setOf( Express(mockk(), mockk(), mockk()), TangemTech(mockk()), + StakeKit(mockk()), // Don't forget to add new config ) @@ -47,6 +49,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { when (it) { is Express -> createExpressModel() is TangemTech -> createTangemTechModel() + is StakeKit -> createStakeKitModel() } } @@ -72,5 +75,12 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { expected = "https://api.tangem-tech.com/v1/", ) } + + private fun createStakeKitModel(): Model { + return Model( + id = ApiConfig.ID.StakeKit, + expected = "https://api.stakek.it/v1/", + ) + } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt index c8bf8c8b56..e2d3b7e6a3 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt @@ -63,9 +63,11 @@ internal class EnvironmentsTogglesViewModel @Inject constructor( } private fun Map.toUiModel(): ImmutableSet { - return map { + return mapNotNull { val (config, currentEnvironment) = it + if (config.environmentConfigs.size <= 1) return@mapNotNull null + EnvironmentTogglesScreenUM.ApiInfoUM( name = config.id.name, select = currentEnvironment.name, From b776c208dda6c942194da26d663b02502aa891b0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Aug 2024 18:26:12 +0300 Subject: [PATCH 64/87] Updated on 2026-08-14 --- .../com/tangem/datasource/di/NetworkModule.kt | 108 +++++++--------- .../config/managers/MockConfigManager.kt | 24 ++++ .../managers/ProdApiConfigsManagerTest.kt | 121 +++++++++++++----- 3 files changed, 164 insertions(+), 89 deletions(-) create mode 100644 core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockConfigManager.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index a393f14ee7..b43900233f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -32,7 +32,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class NetworkModule { +internal object NetworkModule { + + private const val DEV_V1_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/" + private const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/" + private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L @Provides @Singleton @@ -55,20 +59,12 @@ class NetworkModule { @ApplicationContext context: Context, apiConfigsManager: ApiConfigsManager, ): TangemExpressApi { - val environmentConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.Express) - - return Retrofit.Builder() - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .baseUrl(environmentConfig.baseUrl) - .client( - OkHttpClient.Builder() - .applyApiConfig(ApiConfig.ID.Express, apiConfigsManager) - .addLoggers(context) - .build(), - ) - .build() - .create(TangemExpressApi::class.java) + return createApi( + id = ApiConfig.ID.Express, + moshi = moshi, + context = context, + apiConfigsManager = apiConfigsManager, + ) } @Provides @@ -78,20 +74,12 @@ class NetworkModule { @ApplicationContext context: Context, apiConfigsManager: ApiConfigsManager, ): StakeKitApi { - val environmentConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.StakeKit) - - return Retrofit.Builder() - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .baseUrl(environmentConfig.baseUrl) - .client( - OkHttpClient.Builder() - .applyApiConfig(id = ApiConfig.ID.StakeKit, apiConfigsManager = apiConfigsManager) - .addLoggers(context) - .build(), - ) - .build() - .create(StakeKitApi::class.java) + return createApi( + id = ApiConfig.ID.StakeKit, + moshi = moshi, + context = context, + apiConfigsManager = apiConfigsManager, + ) } @Provides @@ -101,36 +89,26 @@ class NetworkModule { @ApplicationContext context: Context, apiConfigsManager: ApiConfigsManager, ): TangemTechApi { - val environmentConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech) - - return Retrofit.Builder() - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .baseUrl(environmentConfig.baseUrl) - .client( - OkHttpClient.Builder() - .applyApiConfig(id = ApiConfig.ID.TangemTech, apiConfigsManager = apiConfigsManager) - .applyTimeoutAnnotations() - .addLoggers(context) - .build(), - ) - .build() - .create(TangemTechApi::class.java) + return createApi( + id = ApiConfig.ID.TangemTech, + moshi = moshi, + context = context, + apiConfigsManager = apiConfigsManager, + ) } + // TODO: It will be deleted in the future or refactored using ApiConfig @Provides @Singleton fun provideTangemTechApiV2( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, appVersionProvider: AppVersionProvider, - apiConfigsManager: ApiConfigsManager, ): TangemTechApiV2 { return provideTangemTechApiInternal( moshi = moshi, context = context, appVersionProvider = appVersionProvider, - apiConfigsManager = apiConfigsManager, baseUrl = PROD_V2_TANGEM_TECH_BASE_URL, ) } @@ -142,17 +120,16 @@ class NetworkModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, appVersionProvider: AppVersionProvider, - apiConfigsManager: ApiConfigsManager, ): TangemTechApi { return provideTangemTechApiInternal( moshi = moshi, context = context, appVersionProvider = appVersionProvider, - apiConfigsManager = apiConfigsManager, baseUrl = DEV_V1_TANGEM_TECH_BASE_URL, ) } + // TODO: [REDACTED_JIRA] @Provides @DevTangemApi @Singleton @@ -160,13 +137,11 @@ class NetworkModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, appVersionProvider: AppVersionProvider, - apiConfigsManager: ApiConfigsManager, ): TangemTechMarketsApi { return provideTangemTechApiInternal( moshi = moshi, context = context, appVersionProvider = appVersionProvider, - apiConfigsManager = apiConfigsManager, baseUrl = DEV_V1_TANGEM_TECH_BASE_URL, timeouts = Timeouts( callTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, @@ -181,13 +156,11 @@ class NetworkModule { moshi: Moshi, context: Context, appVersionProvider: AppVersionProvider, - apiConfigsManager: ApiConfigsManager, baseUrl: String, timeouts: Timeouts = Timeouts(), requestHeaders: List = listOf(AppVersionPlatformHeaders(appVersionProvider)), ): T { val client = OkHttpClient.Builder() - .applyApiConfig(id = ApiConfig.ID.TangemTech, apiConfigsManager = apiConfigsManager) .applyTimeoutAnnotations() .let { builder -> var b = builder @@ -222,18 +195,33 @@ class NetworkModule { .create(T::class.java) } + private inline fun createApi( + id: ApiConfig.ID, + moshi: Moshi, + context: Context, + apiConfigsManager: ApiConfigsManager, + ): T { + val environmentConfig = apiConfigsManager.getEnvironmentConfig(id) + + return Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) + .baseUrl(environmentConfig.baseUrl) + .client( + OkHttpClient.Builder() + .applyApiConfig(id, apiConfigsManager) + .applyTimeoutAnnotations() + .addLoggers(context) + .build(), + ) + .build() + .create(T::class.java) + } + private data class Timeouts( val callTimeoutSeconds: Long? = null, val connectTimeoutSeconds: Long? = null, val readTimeoutSeconds: Long? = null, val writeTimeoutSeconds: Long? = null, ) - - private companion object { - const val DEV_V1_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/" - - const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/" - - const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L - } } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockConfigManager.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockConfigManager.kt new file mode 100644 index 0000000000..4e81af810b --- /dev/null +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockConfigManager.kt @@ -0,0 +1,24 @@ +package com.tangem.datasource.api.common.config.managers + +import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.config.Loader +import com.tangem.datasource.config.models.Config +import com.tangem.datasource.config.models.ConfigModel +import com.tangem.datasource.config.models.ExpressModel + +/** + * Mock [ConfigManager] implementation for [ProdApiConfigsManagerTest] + * +[REDACTED_AUTHOR] + */ +internal class MockConfigManager : ConfigManager { + + override val config = Config( + express = ExpressModel(apiKey = ProdApiConfigsManagerTest.EXPRESS_API_KEY, signVerifierPublicKey = ""), + devExpress = ExpressModel(apiKey = ProdApiConfigsManagerTest.EXPRESS_DEV_API_KEY, signVerifierPublicKey = ""), + ) + + override suspend fun load(configLoader: Loader, onComplete: ((config: Config) -> Unit)?) = Unit + override fun turnOff(name: String) = Unit + override fun resetToDefault(name: String) = Unit +} \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index f5b88829a3..d818bd6c9f 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -2,20 +2,35 @@ package com.tangem.datasource.api.common.config.managers import com.google.common.truth.Truth import com.tangem.datasource.BuildConfig -import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.* import com.tangem.datasource.api.common.config.ApiConfig.Companion.DEBUG_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE -import com.tangem.datasource.api.common.config.Express -import com.tangem.datasource.api.common.config.StakeKit -import com.tangem.datasource.api.common.config.TangemTech +import com.tangem.lib.auth.ExpressAuthProvider +import com.tangem.lib.auth.StakeKitAuthProvider +import com.tangem.utils.Provider +import com.tangem.utils.version.AppVersionProvider +import io.mockk.every import io.mockk.mockk +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized +private val configManager = MockConfigManager() +private val appVersionProvider = mockk() +private val expressAuthProvider = mockk() +private val stakeKitAuthProvider = mockk() + +// Don't forget to add new config !!! +private val API_CONFIGS = setOf( + Express(configManager, expressAuthProvider, appVersionProvider), + TangemTech(appVersionProvider), + StakeKit(stakeKitAuthProvider), +) + /** [REDACTED_AUTHOR] */ @@ -24,24 +39,35 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { private val manager = ProdApiConfigsManager(API_CONFIGS) - @Test - fun test_getBaseUrl() { - val actual = manager.getEnvironmentConfig(id = model.id).baseUrl - - Truth.assertThat(actual).isEqualTo(model.expected) + @Before + fun setup() { + every { appVersionProvider.versionName } returns VERSION_NAME + every { expressAuthProvider.getUserId() } returns EXPRESS_USER_ID + every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID + every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY } - data class Model(val id: ApiConfig.ID, val expected: String) + @Test + fun test_getEnvironmentConfig() { + val actual = manager.getEnvironmentConfig(id = model.id) - private companion object { + Truth.assertThat(actual.environment).isEqualTo(model.expected.environment) + Truth.assertThat(actual.baseUrl).isEqualTo(model.expected.baseUrl) - val API_CONFIGS = setOf( - Express(mockk(), mockk(), mockk()), - TangemTech(mockk()), - StakeKit(mockk()), + Truth.assertThat(actual.headers.mapValues { it.value() }) + .isEqualTo(model.expected.headers.mapValues { it.value() }) + } - // Don't forget to add new config - ) + data class Model(val id: ApiConfig.ID, val expected: ApiEnvironmentConfig) + + internal companion object { + + const val VERSION_NAME = "debug" + const val EXPRESS_USER_ID = "express_user_id" + const val EXPRESS_SESSION_ID = "express_session_id" + const val EXPRESS_API_KEY = "express_api_key" + const val EXPRESS_DEV_API_KEY = "express_dev_api_key" + const val STAKE_KIT_API_KEY = "stake_kit_api_key" @JvmStatic @Parameterized.Parameters @@ -54,32 +80,69 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { } private fun createExpressModel(): Model { + val environment = when (BuildConfig.BUILD_TYPE) { + DEBUG_BUILD_TYPE -> ApiEnvironment.DEV + INTERNAL_BUILD_TYPE, + MOCKED_BUILD_TYPE, + -> ApiEnvironment.STAGE + EXTERNAL_BUILD_TYPE, + RELEASE_BUILD_TYPE, + -> ApiEnvironment.PROD + else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") + } + return Model( id = ApiConfig.ID.Express, - expected = when (BuildConfig.BUILD_TYPE) { - DEBUG_BUILD_TYPE -> "[REDACTED_ENV_URL]" - INTERNAL_BUILD_TYPE, - MOCKED_BUILD_TYPE, - -> "[REDACTED_ENV_URL]" - EXTERNAL_BUILD_TYPE, - RELEASE_BUILD_TYPE, - -> "https://express.tangem.com/v1/" - else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") - }, + expected = ApiEnvironmentConfig( + environment = environment, + baseUrl = when (BuildConfig.BUILD_TYPE) { + DEBUG_BUILD_TYPE -> "[REDACTED_ENV_URL]" + INTERNAL_BUILD_TYPE, + MOCKED_BUILD_TYPE, + -> "[REDACTED_ENV_URL]" + EXTERNAL_BUILD_TYPE, + RELEASE_BUILD_TYPE, + -> "https://express.tangem.com/v1/" + else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") + }, + headers = mapOf( + "api-key" to Provider { + if (environment == ApiEnvironment.PROD) EXPRESS_API_KEY else EXPRESS_DEV_API_KEY + }, + "user-id" to Provider { EXPRESS_USER_ID }, + "session-id" to Provider { EXPRESS_SESSION_ID }, + "version" to Provider { VERSION_NAME }, + "platform" to Provider { "android" }, + ), + ), ) } private fun createTangemTechModel(): Model { return Model( id = ApiConfig.ID.TangemTech, - expected = "https://api.tangem-tech.com/v1/", + expected = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://api.tangem-tech.com/v1/", + headers = mapOf( + "version" to Provider { VERSION_NAME }, + "platform" to Provider { "android" }, + ), + ), ) } private fun createStakeKitModel(): Model { return Model( id = ApiConfig.ID.StakeKit, - expected = "https://api.stakek.it/v1/", + expected = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://api.stakek.it/v1/", + headers = mapOf( + "X-API-KEY" to Provider { STAKE_KIT_API_KEY }, + "accept" to Provider { "application/json" }, + ), + ), ) } } From 831a9ad0e2af7b33b401141281c71925604dd16a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 14:46:01 +0300 Subject: [PATCH 65/87] Updated on 2026-08-14 --- .../extension/AppExtensionConfigurations.kt | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index 1d659a2d09..f3d9c37c3e 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -1,6 +1,7 @@ package com.tangem.plugin.configuration.configurations.extension import com.android.build.gradle.AppExtension +import com.android.build.gradle.internal.dsl.DefaultConfig import com.tangem.plugin.configuration.model.AppConfig import com.tangem.plugin.configuration.model.BuildType import com.tangem.plugin.configuration.utils.BuildConfigFieldFactory @@ -9,15 +10,15 @@ import com.android.build.gradle.internal.dsl.BuildType as AndroidBuildType internal fun AppExtension.configure(project: Project) { configureCompileSdk() - configureDefaultConfig(project) + val defaultConfig = configureDefaultConfig(project) configureBuildFeatures() - configureBuildTypes() + configureBuildTypes(defaultConfig) configurePackagingOptions() configureCompose(project) configureCompilerOptions() } -private fun AppExtension.configureDefaultConfig(project: Project) { +private fun AppExtension.configureDefaultConfig(project: Project): DefaultConfig { defaultConfig { applicationId = AppConfig.packageName minSdk = AppConfig.minSdkVersion @@ -39,6 +40,8 @@ private fun AppExtension.configureDefaultConfig(project: Project) { testInstrumentationRunner = "com.tangem.common.HiltTestRunner" } + + return defaultConfig } // TODO: [REDACTED_JIRA] @@ -48,43 +51,57 @@ private fun AppExtension.configureBuildFeatures() { } } -private fun AppExtension.configureBuildTypes() { +private fun AppExtension.configureBuildTypes(defaultConfig: DefaultConfig) { buildTypes { - BuildType.values().forEach { buildVariant -> - maybeCreate(buildVariant.id).apply { - configureBuildVariant(extension = this@configureBuildTypes, buildVariant) + BuildType.values().forEach { buildType -> + maybeCreate(buildType.id).apply { + configureBuildVariant( + appExtension = this@configureBuildTypes, + buildType = buildType, + defaultConfig = defaultConfig + ) BuildConfigFieldFactory( - fields = buildVariant.configFields, + fields = buildType.configFields, builder = ::buildConfigField, ).create() } } } + testBuildType = BuildType.Mocked.id } -private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buildType: BuildType) { +private fun AndroidBuildType.configureBuildVariant( + appExtension: AppExtension, + buildType: BuildType, + defaultConfig: DefaultConfig, +) { when (buildType) { BuildType.Release -> { isDebuggable = false isMinifyEnabled = false - proguardFiles(extension.getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + proguardFiles(appExtension.getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") } BuildType.Debug -> { isDebuggable = true isMinifyEnabled = false } BuildType.External -> { - initWith(extension.buildTypes.getByName(BuildType.Release.id)) + initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) - signingConfig = extension.signingConfigs.getByName(BuildType.Debug.id) + signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) } - BuildType.Internal, - BuildType.Mocked, - -> { - initWith(extension.buildTypes.getByName(BuildType.Release.id)) + BuildType.Internal -> { + initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) - signingConfig = extension.signingConfigs.getByName(BuildType.Debug.id) + signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) + isDebuggable = true + } + BuildType.Mocked -> { + defaultConfig.versionName = LARGE_VERSION_NAME + initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) + matchingFallbacks.add(BuildType.Mocked.id) + signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) isDebuggable = true } } @@ -102,4 +119,6 @@ private fun AppExtension.configurePackagingOptions() { excludes += "META-INF/gradle/incremental.annotation.processors" } } -} \ No newline at end of file +} + +private const val LARGE_VERSION_NAME = "100.0.0" \ No newline at end of file From 11f32af2958883831306d99b6676c4f090bf7d42 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 15:06:34 +0300 Subject: [PATCH 66/87] Updated on 2026-08-14 --- .../utils/SharedPreferencesKeyMigration.kt | 27 +++---------------- .../core/ui/utils/BigDecimalFormatter.kt | 5 ---- .../tangem/data/visa/utils/CurrencyUtils.kt | 14 +++------- gradle/dependencies.toml | 4 +-- .../plugin/configuration/model/AppConfig.kt | 2 +- 5 files changed, 10 insertions(+), 42 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SharedPreferencesKeyMigration.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SharedPreferencesKeyMigration.kt index f9c11e6471..a049816e5c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SharedPreferencesKeyMigration.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SharedPreferencesKeyMigration.kt @@ -1,9 +1,6 @@ package com.tangem.datasource.local.preferences.utils import android.content.Context -import android.os.Build -import androidx.annotation.DoNotInline -import androidx.annotation.RequiresApi import androidx.datastore.core.DataMigration import androidx.datastore.preferences.core.* import java.io.File @@ -72,27 +69,11 @@ internal class SharedPreferencesKeyMigration( } private fun deleteSharedPreferences(context: Context, name: String) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (!Api24Impl.deleteSharedPreferences(context, name)) { - throw IOException("Unable to delete SharedPreferences: $name") - } - } else { - val prefsFile = getSharedPrefsFile(context, name) - val prefsBackup = getSharedPrefsBackup(prefsFile) + val prefsFile = getSharedPrefsFile(context, name) + val prefsBackup = getSharedPrefsBackup(prefsFile) - prefsFile.delete() - prefsBackup.delete() - } - } - - @RequiresApi(Build.VERSION_CODES.N) - private object Api24Impl { - - @JvmStatic - @DoNotInline - fun deleteSharedPreferences(context: Context, name: String): Boolean { - return context.deleteSharedPreferences(name) - } + prefsFile.delete() + prefsBackup.delete() } private fun getSharedPrefsFile(context: Context, name: String): File { 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 b0c1a83fed..91ef672575 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,7 +1,6 @@ package com.tangem.core.ui.utils import android.icu.text.CompactDecimalFormat -import android.os.Build import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.LOWER_SIGN @@ -366,10 +365,6 @@ object BigDecimalFormatter { threeDigitsMethod: Boolean = false, scale: Int = 0, ): String { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - return BigDecimalFormatterCompat.formatCompactAmountNoLocaleContext(amount = amount) - } - if (threeDigitsMethod) { val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP) val digitsCount = scaledAmount.longValueExact().toString().count() diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt index bcc9aeb311..405d55d6a8 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt @@ -1,15 +1,7 @@ package com.tangem.data.visa.utils -import android.os.Build -import timber.log.Timber import java.util.Currency -internal fun findCurrencyByNumericCode(code: Int): Currency { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - Currency.getAvailableCurrencies().firstOrNull { it.numericCode == code } - ?: Currency.getInstance(VisaConstants.fiatCurrency.code) - } else { - Timber.w("Unable to get currency by numeric code on API level ${Build.VERSION.SDK_INT}") - Currency.getInstance(VisaConstants.fiatCurrency.code) - } -} \ No newline at end of file +internal fun findCurrencyByNumericCode(code: Int) = + Currency.getAvailableCurrencies().firstOrNull { it.numericCode == code } + ?: Currency.getInstance(VisaConstants.fiatCurrency.code) \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 6201894fe4..74b8993062 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,9 +88,9 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-734" +tangemBlockchainSdk = "develop-736" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-375" +tangemCardSdk = "develop-378" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem16" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt index c8bfbff0f1..ad8aa17560 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt @@ -4,7 +4,7 @@ internal object AppConfig { const val packageName = "com.tangem.wallet" const val versionCode = 1 const val versionName = "1.0.0-SNAPSHOT" - const val minSdkVersion = 23 + const val minSdkVersion = 24 const val targetSdkVersion = 34 const val compileSdkVersion = 34 } \ No newline at end of file From 3e341569253d78a6b5381ae520cdd9db28243899 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 12:13:02 +0500 Subject: [PATCH 67/87] Updated on 2026-08-14 --- .../impl/presentation/state/SendAlertState.kt | 7 --- .../state/SendEventStateFactory.kt | 3 - .../presentation/state/SendNotification.kt | 8 +++ .../state/SendTransactionAlertConverter.kt | 10 ---- .../state/confirm/SendNotificationFactory.kt | 55 ++++++++++--------- .../presentation/viewmodel/SendViewModel.kt | 1 - 6 files changed, 38 insertions(+), 46 deletions(-) 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 fc7ae7daf1..4abbd3c665 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 @@ -68,13 +68,6 @@ internal sealed class SendAlertState { override val confirmButtonText: TextReference = resourceReference(R.string.common_continue) } - data class ReserveAmount(val amount: String) : SendAlertState() { - override val title: TextReference = - resourceReference(id = R.string.send_notification_invalid_reserve_amount_title, wrappedList(amount)) - override val message: TextReference = - resourceReference(id = R.string.send_notification_invalid_reserve_amount_text) - } - data class FeeUnreachableError( override val onConfirmClick: (() -> Unit), ) : SendAlertState() { 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 852339a7ed..3424853d60 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.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 @@ -22,13 +21,11 @@ import java.math.BigDecimal internal class SendEventStateFactory( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, private val clickIntents: SendClickIntents, private val feeStateFactory: FeeStateFactory, ) { private val sendTransactionErrorConverter by lazy(LazyThreadSafetyMode.NONE) { SendTransactionAlertConverter( - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, clickIntents = clickIntents, ) } 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 017c91f8d4..8a455b1b25 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 @@ -105,6 +105,14 @@ internal sealed class SendNotification(val config: NotificationConfig) { onClick = onConfirmClick, ), ) + + data class ReserveAmount(val amount: String) : Error( + title = resourceReference( + id = R.string.send_notification_invalid_reserve_amount_title, + wrappedList(amount), + ), + subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text), + ) } sealed class Warning( 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 7f6a8588b1..d37954025b 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,14 +1,10 @@ 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? { @@ -42,12 +38,6 @@ internal class SendTransactionAlertConverter( cause = value.ex?.localizedMessage, onConfirmClick = { clickIntents.onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) }, ) - 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/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index a4e3bcbf9c..54dd992fa4 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 @@ -31,6 +31,7 @@ import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.isTezos import com.tangem.utils.Provider +import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -60,14 +61,15 @@ internal class SendNotificationFactory( .map { val state = currentStateProvider() val isEditState = stateRouterProvider().isEditState - val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val balance = cryptoCurrencyStatusProvider().value.amount.orZero() val sendState = state.sendState ?: return@map persistentListOf() val feeState = state.getFeeState(isEditState) ?: return@map persistentListOf() val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return@map persistentListOf() - val amountValue = amountState.amountTextField.cryptoAmount.value ?: BigDecimal.ZERO - val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO - val reduceAmountBy = sendState.reduceAmountBy ?: BigDecimal.ZERO + val recipientAddress = state.recipientState?.addressTextField?.value.orEmpty() + val amountValue = amountState.amountTextField.cryptoAmount.value.orZero() + val feeValue = feeState.fee?.amount?.value.orZero() + val reduceAmountBy = sendState.reduceAmountBy.orZero() val isFeeCoverage = checkFeeCoverage( isSubtractAvailable = isSubtractAvailableProvider(), balance = balance, @@ -89,6 +91,7 @@ internal class SendNotificationFactory( addExceedsBalanceNotification(feeState.fee) addDustWarningNotificationForSpecificBlockchains(feeValue, sendingAmount) addTransactionLimitErrorNotification(feeValue, sendingAmount) + addReserveAmountErrorNotification(recipientAddress, sendingAmount) // warnings addExistentialWarningNotification(feeValue, amountValue) @@ -156,27 +159,29 @@ internal class SendNotificationFactory( } } - // todo temporarily disabling notification - // private suspend fun MutableList.addReserveAmountErrorNotification(recipientAddress: String) { - // val userWalletId = userWalletProvider().walletId - // val cryptoCurrency = cryptoCurrencyStatusProvider().currency - // val isAccountFunded = currencyChecksRepository.checkIfAccountFunded( - // userWalletId, - // cryptoCurrency.network, - // recipientAddress, - // ) - // val minimumAmount = currencyChecksRepository.getReserveAmount(userWalletId, cryptoCurrency.network) - // if (!isAccountFunded && minimumAmount != null && minimumAmount > BigDecimal.ZERO) { - // add( - // SendNotification.Error.ReserveAmountError( - // BigDecimalFormatter.formatCryptoAmount( - // cryptoAmount = minimumAmount, - // cryptoCurrency = cryptoCurrency, - // ), - // ), - // ) - // } - // } + private suspend fun MutableList.addReserveAmountErrorNotification( + recipientAddress: String, + sendingAmount: BigDecimal, + ) { + val userWalletId = userWalletProvider().walletId + val cryptoCurrency = cryptoCurrencyStatusProvider().currency + val isAccountFunded = currencyChecksRepository.checkIfAccountFunded( + userWalletId, + cryptoCurrency.network, + recipientAddress, + ) + val minimumAmount = currencyChecksRepository.getReserveAmount(userWalletId, cryptoCurrency.network) + if (!isAccountFunded && minimumAmount != null && minimumAmount > sendingAmount) { + add( + SendNotification.Error.ReserveAmount( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = minimumAmount, + cryptoCurrency = cryptoCurrency, + ), + ), + ) + } + } private suspend fun MutableList.addTransactionLimitErrorNotification( feeAmount: BigDecimal, 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 a57a241ebf..764197090e 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 @@ -161,7 +161,6 @@ internal class SendViewModel @Inject constructor( clickIntents = this, stateRouterProvider = Provider { stateRouter }, currentStateProvider = Provider { uiState.value }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, feeStateFactory = feeStateFactory, ) From fe09587f6d1bad4d7531f1fc2d7b7c7234309c84 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Aug 2024 21:41:01 +0400 Subject: [PATCH 68/87] Updated on 2026-08-14 --- .../analytics/TokenExchangeAnalyticsEvent.kt | 2 +- .../analytics/TokenReceiveAnalyticsEvent.kt | 2 +- .../analytics/TokenScreenAnalyticsEvent.kt | 2 +- .../analytics/TokenSwapPromoAnalyticsEvent.kt | 2 +- .../analytics/TokenDetailsNotificationsAnalyticsSender.kt | 2 +- .../factory/TokenDetailsSwapTransactionsStateConverter.kt | 2 +- .../tokendetails/viewmodels/ExchangeStatusFactory.kt | 2 +- .../tokendetails/viewmodels/TokenDetailsViewModel.kt | 8 ++++---- .../presentation/deeplink/WalletDeepLinksHandler.kt | 2 +- .../analytics/utils/WalletWarningsAnalyticsSender.kt | 2 +- .../intents/WalletCurrencyActionsClickIntents.kt | 4 ++-- .../viewmodels/intents/WalletWarningsClickIntents.kt | 2 +- 12 files changed, 16 insertions(+), 16 deletions(-) rename domain/tokens/models/src/main/java/com/tangem/domain/tokens/{models => model}/analytics/TokenExchangeAnalyticsEvent.kt (95%) rename domain/tokens/models/src/main/java/com/tangem/domain/tokens/{models => model}/analytics/TokenReceiveAnalyticsEvent.kt (92%) rename domain/tokens/models/src/main/java/com/tangem/domain/tokens/{models => model}/analytics/TokenScreenAnalyticsEvent.kt (97%) rename domain/tokens/models/src/main/java/com/tangem/domain/tokens/{models => model}/analytics/TokenSwapPromoAnalyticsEvent.kt (96%) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenExchangeAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt similarity index 95% rename from domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenExchangeAnalyticsEvent.kt rename to domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt index 260c9ce93f..b89306aded 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenExchangeAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.tokens.models.analytics +package com.tangem.domain.tokens.model.analytics import com.tangem.core.analytics.models.AnalyticsEvent diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenReceiveAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt similarity index 92% rename from domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenReceiveAnalyticsEvent.kt rename to domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt index e5c1668624..f97493ffa2 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenReceiveAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.tokens.models.analytics +package com.tangem.domain.tokens.model.analytics import com.tangem.core.analytics.models.AnalyticsEvent diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt similarity index 97% rename from domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt rename to domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index 171f60eb9e..1896d2c250 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.tokens.models.analytics +package com.tangem.domain.tokens.model.analytics import com.tangem.core.analytics.models.AnalyticsEvent diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt similarity index 96% rename from domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt rename to domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt index 9e9156c366..6e48ed4272 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.tokens.models.analytics +package com.tangem.domain.tokens.model.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index 878dc66047..300083d876 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -4,7 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index d640fc414e..6a8a7087ed 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index fc2733a1ba..67920c922a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -7,7 +7,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase 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 67e0caa285..ca70ba95b1 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 @@ -43,10 +43,10 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent -import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent -import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent -import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.transaction.error.AssociateAssetError import com.tangem.domain.transaction.usecase.AssociateAssetUseCase 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 3c1e89fbeb..d34c68bd50 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 @@ -13,7 +13,7 @@ import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.CoroutineScope 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 1a15d9ee54..2686a1ec9d 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 @@ -3,7 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 9ec62dbe52..98590b9a89 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -27,8 +27,8 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent -import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index b8ee6c5ef5..0e557c8e46 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -14,7 +14,7 @@ import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.models.UnlockWalletsError import com.tangem.domain.wallets.models.UserWallet From 605f17c8737f528d4d5479eedcd095f743321643 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 9 Aug 2024 19:29:13 +0400 Subject: [PATCH 69/87] Updated on 2026-08-14 --- .../tap/common/redux/legacy/LegacyMiddleware.kt | 10 +++++----- .../com/tangem/domain}/utils/BigDecimalUtils.kt | 8 ++++---- domain/tokens/models/build.gradle.kts | 13 +------------ .../tangem/domain/tokens/model/NetworkAddress.kt | 8 -------- .../usecase/CreateApprovalTransactionUseCase.kt | 2 +- .../state/confirm/SendNotificationFactory.kt | 4 ++-- .../impl/presentation/viewmodel/SendViewModel.kt | 4 ++-- .../feature/swap/domain/SwapInteractorImpl.kt | 8 ++++---- 8 files changed, 19 insertions(+), 38 deletions(-) rename domain/{tokens/models/src/main/java/com/tangem/domain/tokens => legacy/src/main/java/com/tangem/domain}/utils/BigDecimalUtils.kt (72%) diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 80d9f694c8..e4bf1086bd 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -3,7 +3,7 @@ package com.tangem.tap.common.redux.legacy import com.tangem.blockchain.common.AmountType import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.redux.LegacyAction -import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.utils.convertToSdkAmount import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.feedback.FeedbackEmail @@ -42,7 +42,7 @@ internal object LegacyMiddleware { is LegacyAction.SendEmailTransactionFailed -> { if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) { - val amount = action.amount?.convertToAmount(action.cryptoCurrency) + val amount = action.amount?.convertToSdkAmount(action.cryptoCurrency) store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke( error = BlockchainErrorInfo( errorMessage = action.errorMessage, @@ -55,7 +55,7 @@ internal object LegacyMiddleware { "" }, amount = amount?.value?.stripZeroPlainString() ?: "unknown", - fee = action.fee?.convertToAmount(action.cryptoCurrency) + fee = action.fee?.convertToSdkAmount(action.cryptoCurrency) ?.value?.stripZeroPlainString() ?: "unknown", ), ) @@ -73,8 +73,8 @@ internal object LegacyMiddleware { )?.let { walletManager -> store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( walletManager = walletManager, - amountToSend = action.amount?.convertToAmount(action.cryptoCurrency), - feeAmount = action.fee?.convertToAmount(action.cryptoCurrency), + amountToSend = action.amount?.convertToSdkAmount(action.cryptoCurrency), + feeAmount = action.fee?.convertToSdkAmount(action.cryptoCurrency), destinationAddress = action.destinationAddress, ) } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt b/domain/legacy/src/main/java/com/tangem/domain/utils/BigDecimalUtils.kt similarity index 72% rename from domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt rename to domain/legacy/src/main/java/com/tangem/domain/utils/BigDecimalUtils.kt index fe93d1d8ec..ad26bc198e 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/utils/BigDecimalUtils.kt @@ -1,13 +1,13 @@ -package com.tangem.domain.tokens.utils +package com.tangem.domain.utils -import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Token import com.tangem.domain.tokens.model.CryptoCurrency import java.math.BigDecimal +import com.tangem.blockchain.common.Amount as SdkAmount -/** Converts `BigDecimal` [cryptoCurrency] to [Amount] */ -fun BigDecimal.convertToAmount(cryptoCurrency: CryptoCurrency) = Amount( +/** Converts `BigDecimal` [cryptoCurrency] to [SdkAmount] */ +fun BigDecimal.convertToSdkAmount(cryptoCurrency: CryptoCurrency): SdkAmount = SdkAmount( currencySymbol = cryptoCurrency.symbol, value = this, decimals = cryptoCurrency.decimals, diff --git a/domain/tokens/models/build.gradle.kts b/domain/tokens/models/build.gradle.kts index c51f155acf..92950e7beb 100644 --- a/domain/tokens/models/build.gradle.kts +++ b/domain/tokens/models/build.gradle.kts @@ -1,14 +1,9 @@ plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.jvm) alias(deps.plugins.kotlin.serialization) id("configuration") } -android { - namespace = "com.tangem.domain.tokens.model" -} - dependencies { /** Project - Core */ implementation(projects.core.analytics.models) @@ -17,13 +12,7 @@ dependencies { implementation(projects.domain.txhistory.models) implementation(projects.domain.staking.models) - /** SDK dependencies */ - implementation(deps.tangem.blockchain) { - exclude(module = "joda-time") - } - /** Other dependencies */ implementation(deps.kotlin.serialization) implementation(deps.jodatime) - implementation(deps.timber) } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkAddress.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkAddress.kt index 9b57fcfe15..2e21ba5001 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkAddress.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkAddress.kt @@ -1,7 +1,5 @@ package com.tangem.domain.tokens.model -import timber.log.Timber - /** * Represents a network address configuration. */ @@ -47,11 +45,5 @@ sealed class NetworkAddress { enum class Type { Primary, Secondary, } - - init { - if (value.isEmpty()) { - Timber.w("Address value is blank") - } - } } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt index 01cf52d680..c884ee6124 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt @@ -3,8 +3,8 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.utils.convertToAmount import com.tangem.domain.wallets.models.UserWalletId import java.math.BigDecimal 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 54dd992fa4..ae185f4c0c 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,8 +20,8 @@ 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.tokens.utils.convertToAmount import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase +import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents @@ -447,7 +447,7 @@ internal class SendNotificationFactory( val sendingCurrency = cryptoCurrencyStatusProvider().currency validateTransactionUseCase( - amount = sendingAmount.convertToAmount(sendingCurrency), + amount = sendingAmount.convertToSdkAmount(sendingCurrency), fee = fee ?: return, memo = state.recipientState?.memoTextField?.value, destination = requireNotNull(state.recipientState?.addressTextField?.value), 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 764197090e..4c9d9bfe6a 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 @@ -29,12 +29,12 @@ 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 -import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.ValidateAddressError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase +import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -855,7 +855,7 @@ internal class SendViewModel @Inject constructor( viewModelScope.launch { createTransactionUseCase( - amount = receivingAmount.convertToAmount(cryptoCurrency), + amount = receivingAmount.convertToSdkAmount(cryptoCurrency), fee = fee, memo = memo, destination = recipient, 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 c0d6a4c835..de0468d9ff 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 @@ -22,11 +22,11 @@ import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.models.TransactionType import com.tangem.domain.transaction.usecase.* +import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -216,7 +216,7 @@ internal class SwapInteractorImpl @Inject constructor( permissionOptions.approveData.approveData } val approveTransaction = createTransactionUseCase( - amount = BigDecimal.ZERO.convertToAmount(permissionOptions.fromToken), + amount = BigDecimal.ZERO.convertToSdkAmount(permissionOptions.fromToken), fee = getFeeForTransaction( fee = permissionOptions.txFee, blockchain = Blockchain.fromId(permissionOptions.fromToken.network.id.value), @@ -518,7 +518,7 @@ internal class SwapInteractorImpl @Inject constructor( ) validateTransactionUseCase( - amount = amount.value.convertToAmount(fromToken), + amount = amount.value.convertToSdkAmount(fromToken), fee = fee, memo = null, destination = getTokenAddress(fromToken), @@ -791,7 +791,7 @@ internal class SwapInteractorImpl @Inject constructor( if (demoConfig.isDemoCardId(cardId)) return SwapTransactionState.UnknownError val txData = createTransactionUseCase( - amount = amount.value.convertToAmount(currencyToSend.currency), + amount = amount.value.convertToSdkAmount(currencyToSend.currency), fee = getFeeForTransaction( fee = txFee, blockchain = Blockchain.fromId(currencyToSend.currency.network.id.value), From 517a9609ec0fe8b1e3a3d8e39bb4600bb9d0cc62 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 12:44:44 +0400 Subject: [PATCH 70/87] Updated on 2026-08-14 --- .../tangem/tap/di/domain/TokensDomainModule.kt | 6 ++++++ .../repository/DefaultCurrenciesRepository.kt | 15 +++++++++++++++ .../tokens/CheckHasLinkedTokensUseCase.kt | 17 +++++++++++++++++ .../tokens/repository/CurrenciesRepository.kt | 2 ++ .../repository/MockCurrenciesRepository.kt | 4 ++++ 5 files changed, 44 insertions(+) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/CheckHasLinkedTokensUseCase.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 09792b495e..24c222149b 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 @@ -413,4 +413,10 @@ internal object TokensDomainModule { quotesRepository = quotesRepository, ) } + + @Provides + @Singleton + fun provideCheckHasLinkedTokensUseCase(currenciesRepository: CurrenciesRepository): CheckHasLinkedTokensUseCase { + return CheckHasLinkedTokensUseCase(currenciesRepository) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index b24c5226bc..238ab2676a 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -454,6 +454,21 @@ internal class DefaultCurrenciesRepository( ) ?: error("Unable to create token") } + override suspend fun hasTokens(userWalletId: UserWalletId, network: Network): Boolean { + val userWallet = getUserWallet(userWalletId) + fetchTokensIfCacheExpired(userWallet, refresh = false) + + val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + } + + return storedTokens.tokens.any { + it.contractAddress != null && + it.networkId == network.backendId && + it.derivationPath == network.derivationPath.value + } + } + private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { return userTokensStore.get(userWallet.walletId).map { storedTokens -> responseCurrenciesFactory.createCurrencies( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/CheckHasLinkedTokensUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/CheckHasLinkedTokensUseCase.kt new file mode 100644 index 0000000000..503731cabc --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/CheckHasLinkedTokensUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId + +class CheckHasLinkedTokensUseCase( + private val currenciesRepository: CurrenciesRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either { + return Either.catch { + currenciesRepository.hasTokens(userWalletId, network) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 4d8d4f0522..37efae4b4b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -223,4 +223,6 @@ interface CurrenciesRepository { contractAddress: String, networkId: String, ): CryptoCurrency.Token + + suspend fun hasTokens(userWalletId: UserWalletId, network: Network): Boolean } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index c1c3aa5e5a..8ef1dfea78 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -144,4 +144,8 @@ internal class MockCurrenciesRepository( ): CryptoCurrency.Token { error("not implemented") } + + override suspend fun hasTokens(userWalletId: UserWalletId, network: Network): Boolean { + return false + } } \ No newline at end of file From 91fbe78c9e1f4c7137ec21eae935edca3b13bff3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Aug 2024 13:03:51 +0400 Subject: [PATCH 71/87] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + domain/manage-tokens/.gitignore | 1 + domain/manage-tokens/build.gradle.kts | 9 +++++++++ domain/manage-tokens/models/.gitignore | 1 + domain/manage-tokens/models/build.gradle.kts | 4 ++++ settings.gradle.kts | 2 ++ 6 files changed, 18 insertions(+) create mode 100644 domain/manage-tokens/.gitignore create mode 100644 domain/manage-tokens/build.gradle.kts create mode 100644 domain/manage-tokens/models/.gitignore create mode 100644 domain/manage-tokens/models/build.gradle.kts diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a003607888..968acfe32e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { implementation(projects.domain.staking) implementation(projects.domain.walletConnect) implementation(projects.domain.markets) + implementation(projects.domain.manageTokens) implementation(projects.common) implementation(projects.common.routing) diff --git a/domain/manage-tokens/.gitignore b/domain/manage-tokens/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/manage-tokens/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts new file mode 100644 index 0000000000..a0f0ac3368 --- /dev/null +++ b/domain/manage-tokens/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + api(projects.domain.manageTokens.models) + api(projects.domain.core) +} \ No newline at end of file diff --git a/domain/manage-tokens/models/.gitignore b/domain/manage-tokens/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/manage-tokens/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/manage-tokens/models/build.gradle.kts b/domain/manage-tokens/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/manage-tokens/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/settings.gradle.kts b/settings.gradle.kts index 0d6be44673..a4720852bf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -226,6 +226,8 @@ include(":domain:staking:models") include(":domain:wallet-connect") include(":domain:markets") include(":domain:markets:models") +include(":domain:manage-tokens") +include(":domain:manage-tokens:models") // endregion Domain modules // region Data modules From 39a9be15215d9d7e6f80b97093724c7a671dd633 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Aug 2024 13:21:42 +0400 Subject: [PATCH 72/87] Updated on 2026-08-14 --- .../model/ManagedCryptoCurrency.kt | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt diff --git a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt new file mode 100644 index 0000000000..daedbea3c1 --- /dev/null +++ b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt @@ -0,0 +1,94 @@ +package com.tangem.domain.managetokens.model + +sealed class ManagedCryptoCurrency { + + abstract val id: ID + abstract val name: String + abstract val symbol: String + + data class Custom( + override val id: ID, + override val name: String, + override val symbol: String, + val contractAddress: String?, + val network: SourceNetwork, + ) : ManagedCryptoCurrency() + + data class Token( + override val id: ID, + override val name: String, + override val symbol: String, + val iconUrl: String, + val availableNetworks: List, + val addedIn: Set, + ) : ManagedCryptoCurrency() { + + val isAdded: Boolean = addedIn.isNotEmpty() + } + + @JvmInline + value class ID(val value: String) { + + constructor( + networkId: SourceNetwork.ID, + contractAddress: String?, + derivationPath: String?, + ) : this( + value = buildString { + if (contractAddress.isNullOrBlank()) { + append(CUSTOM_COIN_PREFIX) + } else { + append(CUSTOM_TOKEN_PREFIX) + } + + append(DIVIDER) + append(networkId.value) + + if (!contractAddress.isNullOrBlank()) { + append(DIVIDER) + append(contractAddress) + } + + if (!derivationPath.isNullOrBlank()) { + append(DIVIDER) + append(derivationPath.hashCode()) + } + }, + ) + + companion object { + + const val CUSTOM_TOKEN_PREFIX = "custom_token" + const val CUSTOM_COIN_PREFIX = "custom_coin" + const val DIVIDER = "_" + } + } + + data class SourceNetwork( + val id: ID, + val name: String, + val iconUrl: String, + val type: Type, + ) { + + @JvmInline + value class ID(val value: String) + + sealed class Type(val name: String?) { + + open val contractAddress: String? = null + + data object MAIN : Type(name = "MAIN") + + data class ERC20(override val contractAddress: String) : Type(name = "ERC20") + + data class TRC20(override val contractAddress: String) : Type(name = "TRC20") + + data class BEP20(override val contractAddress: String) : Type(name = "BEP20") + + data class BEP2(override val contractAddress: String) : Type(name = "BEP2") + + data class Other(override val contractAddress: String) : Type(name = null) + } + } +} \ No newline at end of file From 93be79ff0ef2cf0ca8bf6a1a44d1213b34efe4dc Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Aug 2024 21:40:29 +0400 Subject: [PATCH 73/87] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 20 +++++++++++++++++++ domain/manage-tokens/build.gradle.kts | 6 ++++++ .../managetokens/GetManagedTokensUseCase.kt | 14 +++++++++++++ .../model/ManageTokensListConfig.kt | 8 ++++++++ .../model/ManageTokensTypealiases.kt | 8 ++++++++ .../model/ManageTokensUpdateAction.kt | 12 +++++++++++ .../repository/ManageTokensRepository.kt | 9 +++++++++ 7 files changed, 77 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensListConfig.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensTypealiases.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensUpdateAction.kt create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt new file mode 100644 index 0000000000..dd78da6dfd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.managetokens.GetManagedTokensUseCase +import com.tangem.domain.managetokens.repository.ManageTokensRepository +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 ManageTokensDomainModule { + + @Provides + @Singleton + fun provideGetManageTokensUseCase(manageTokensRepository: ManageTokensRepository): GetManagedTokensUseCase { + return GetManagedTokensUseCase(manageTokensRepository) + } +} \ No newline at end of file diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index a0f0ac3368..cea5a5fc27 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -4,6 +4,12 @@ plugins { } dependencies { + + /* Domain */ api(projects.domain.manageTokens.models) api(projects.domain.core) + implementation(projects.domain.wallets.models) + + /* Core */ + api(projects.core.pagination) } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt new file mode 100644 index 0000000000..62a5647d18 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.managetokens + +import com.tangem.domain.managetokens.model.ManageTokensListBatchFlow +import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext +import com.tangem.domain.managetokens.repository.ManageTokensRepository + +class GetManagedTokensUseCase( + private val repository: ManageTokensRepository, +) { + + operator fun invoke(context: ManageTokensListBatchingContext, batchSize: Int = 40): ManageTokensListBatchFlow { + return repository.tokensFlow(context, batchSize) + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensListConfig.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensListConfig.kt new file mode 100644 index 0000000000..6b4dc9dca9 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensListConfig.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.managetokens.model + +import com.tangem.domain.wallets.models.UserWalletId + +data class ManageTokensListConfig( + val userWalletId: UserWalletId?, + val searchText: String?, +) \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensTypealiases.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensTypealiases.kt new file mode 100644 index 0000000000..0058f82863 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensTypealiases.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.managetokens.model + +import com.tangem.pagination.BatchFlow +import com.tangem.pagination.BatchingContext + +typealias ManageTokensListBatchingContext = BatchingContext + +typealias ManageTokensListBatchFlow = BatchFlow, ManageTokensUpdateAction> \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensUpdateAction.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensUpdateAction.kt new file mode 100644 index 0000000000..75b1b2e4ad --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensUpdateAction.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.managetokens.model + +import com.tangem.domain.tokens.model.Network + +sealed class ManageTokensUpdateAction { + + data class AddCurrency( + val currencyId: ManagedCryptoCurrency.ID, + val networkId: Network.ID, + val isSelected: Boolean, + ) : ManageTokensUpdateAction() +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt new file mode 100644 index 0000000000..d993ad5810 --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.managetokens.repository + +import com.tangem.domain.managetokens.model.ManageTokensListBatchFlow +import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext + +interface ManageTokensRepository { + + fun tokensFlow(context: ManageTokensListBatchingContext, batchSize: Int): ManageTokensListBatchFlow +} \ No newline at end of file From da309aadfad497ec54f3db3d80cdab6142ff003e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Aug 2024 12:57:20 +0400 Subject: [PATCH 74/87] Updated on 2026-08-14 --- domain/manage-tokens/build.gradle.kts | 1 + domain/manage-tokens/models/build.gradle.kts | 6 + .../model/ManagedCryptoCurrency.kt | 125 +++++++++--------- 3 files changed, 66 insertions(+), 66 deletions(-) diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index cea5a5fc27..b24f993032 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -9,6 +9,7 @@ dependencies { api(projects.domain.manageTokens.models) api(projects.domain.core) implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) /* Core */ api(projects.core.pagination) diff --git a/domain/manage-tokens/models/build.gradle.kts b/domain/manage-tokens/models/build.gradle.kts index 7ff7fb7522..90f5e66f52 100644 --- a/domain/manage-tokens/models/build.gradle.kts +++ b/domain/manage-tokens/models/build.gradle.kts @@ -1,4 +1,10 @@ plugins { alias(deps.plugins.kotlin.jvm) id("configuration") +} + +dependencies { + + /* Domain */ + implementation(projects.domain.tokens.models) } \ No newline at end of file diff --git a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt index daedbea3c1..f7f82002cf 100644 --- a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt +++ b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt @@ -1,94 +1,87 @@ package com.tangem.domain.managetokens.model +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network + sealed class ManagedCryptoCurrency { abstract val id: ID abstract val name: String abstract val symbol: String + abstract val iconUrl: String? - data class Custom( - override val id: ID, - override val name: String, - override val symbol: String, - val contractAddress: String?, - val network: SourceNetwork, - ) : ManagedCryptoCurrency() + sealed class Custom : ManagedCryptoCurrency() { + + abstract val currencyId: CryptoCurrency.ID + abstract val network: Network + + override val id: ID + get() = ID(currencyId.value) + + data class Token( + override val currencyId: CryptoCurrency.ID, + override val name: String, + override val symbol: String, + override val iconUrl: String?, + override val network: Network, + val contractAddress: String, + ) : Custom() + + data class Coin( + override val currencyId: CryptoCurrency.ID, + override val name: String, + override val symbol: String, + override val iconUrl: String?, + override val network: Network, + ) : Custom() + } data class Token( override val id: ID, override val name: String, override val symbol: String, - val iconUrl: String, + override val iconUrl: String, val availableNetworks: List, - val addedIn: Set, + val addedIn: Set, ) : ManagedCryptoCurrency() { val isAdded: Boolean = addedIn.isNotEmpty() } @JvmInline - value class ID(val value: String) { + value class ID(val value: String) - constructor( - networkId: SourceNetwork.ID, - contractAddress: String?, - derivationPath: String?, - ) : this( - value = buildString { - if (contractAddress.isNullOrBlank()) { - append(CUSTOM_COIN_PREFIX) - } else { - append(CUSTOM_TOKEN_PREFIX) + sealed class SourceNetwork { + + abstract val network: Network + + val id: Network.ID + get() = network.id + + val typeName: String + get() = when (this) { + is Main -> MAIN_NETWORK_TYPE_NAME + is Default -> when (network.standardType) { + is Network.StandardType.BEP2, + is Network.StandardType.BEP20, + is Network.StandardType.ERC20, + is Network.StandardType.TRC20, + -> network.standardType.name + is Network.StandardType.Unspecified -> "" } + } - append(DIVIDER) - append(networkId.value) + data class Main( + override val network: Network, + ) : SourceNetwork() - if (!contractAddress.isNullOrBlank()) { - append(DIVIDER) - append(contractAddress) - } + data class Default( + override val network: Network, + val contractAddress: String, + ) : SourceNetwork() - if (!derivationPath.isNullOrBlank()) { - append(DIVIDER) - append(derivationPath.hashCode()) - } - }, - ) - - companion object { - - const val CUSTOM_TOKEN_PREFIX = "custom_token" - const val CUSTOM_COIN_PREFIX = "custom_coin" - const val DIVIDER = "_" - } - } - - data class SourceNetwork( - val id: ID, - val name: String, - val iconUrl: String, - val type: Type, - ) { - - @JvmInline - value class ID(val value: String) - - sealed class Type(val name: String?) { - - open val contractAddress: String? = null - - data object MAIN : Type(name = "MAIN") - - data class ERC20(override val contractAddress: String) : Type(name = "ERC20") - - data class TRC20(override val contractAddress: String) : Type(name = "TRC20") - - data class BEP20(override val contractAddress: String) : Type(name = "BEP20") - - data class BEP2(override val contractAddress: String) : Type(name = "BEP2") - - data class Other(override val contractAddress: String) : Type(name = null) + private companion object { + const val MAIN_NETWORK_TYPE_NAME = "MAIN" } } } \ No newline at end of file From 0eec777144e2c731fb378ccb166b0309ed36e2a0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 12:50:39 +0400 Subject: [PATCH 75/87] Updated on 2026-08-14 --- .../com/tangem/data/tokens/utils/NetworkStatusFactory.kt | 4 ++++ .../com/tangem/domain/managetokens/GetManagedTokensUseCase.kt | 2 +- .../domain/managetokens/repository/ManageTokensRepository.kt | 2 +- .../transaction/usecase/CreateApprovalTransactionUseCase.kt | 4 ++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt index 81905be776..0aa2528c5c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -122,6 +122,10 @@ internal class NetworkStatusFactory { Address.Type.Secondary -> NetworkAddress.Address.Type.Secondary } + if (address.value.isBlank()) { + Timber.w("Address value is blank") + } + return NetworkAddress.Address(address.value, type) } } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt index 62a5647d18..b46140c043 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt @@ -9,6 +9,6 @@ class GetManagedTokensUseCase( ) { operator fun invoke(context: ManageTokensListBatchingContext, batchSize: Int = 40): ManageTokensListBatchFlow { - return repository.tokensFlow(context, batchSize) + return repository.getTokenListBatchFlow(context, batchSize) } } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt index d993ad5810..3104d98d64 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/ManageTokensRepository.kt @@ -5,5 +5,5 @@ import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext interface ManageTokensRepository { - fun tokensFlow(context: ManageTokensListBatchingContext, batchSize: Int): ManageTokensListBatchFlow + fun getTokenListBatchFlow(context: ManageTokensListBatchingContext, batchSize: Int): ManageTokensListBatchFlow } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt index c884ee6124..2a699deea3 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateApprovalTransactionUseCase.kt @@ -4,7 +4,7 @@ import arrow.core.Either import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.transaction.TransactionRepository -import com.tangem.domain.utils.convertToAmount +import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWalletId import java.math.BigDecimal @@ -23,7 +23,7 @@ class CreateApprovalTransactionUseCase( hash: String? = null, ) = Either.catch { transactionRepository.createApprovalTransaction( - amount = amount.convertToAmount(cryptoCurrency), + amount = amount.convertToSdkAmount(cryptoCurrency), contractAddress = contractAddress, spenderAddress = spenderAddress, userWalletId = userWalletId, From bec0636e51a642ea717db5cd4adca34d48d7f063 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 15:35:33 +0500 Subject: [PATCH 76/87] Updated on 2026-08-14 --- .../presentation/viewmodel/SendViewModel.kt | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 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 4c9d9bfe6a..68cbb4d8dc 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 @@ -35,6 +35,8 @@ import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.utils.convertToSdkAmount +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -56,6 +58,7 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -75,6 +78,8 @@ internal class SendViewModel @Inject constructor( private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, + private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendTransactionUseCase: SendTransactionUseCase, @@ -194,7 +199,6 @@ internal class SendViewModel @Inject constructor( ) } - // todo convert to StateFlow val uiState: MutableStateFlow = MutableStateFlow( value = stateFactory.getInitialState(), ) @@ -932,7 +936,8 @@ internal class SendViewModel @Inject constructor( coroutineScope.launch { // we should update network to find pending tx after 1 sec fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrency.network)) - // we should update network for new balance + // we should update tx history and network for new balance + updateTxHistory() updateDelayedCurrencyStatusUseCase( userWalletId = userWallet.walletId, network = cryptoCurrency.network, @@ -942,6 +947,22 @@ internal class SendViewModel @Inject constructor( } } + private suspend fun updateTxHistory() { + delay(BALANCE_UPDATE_DELAY) + val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + ) + + txHistoryItemsCountEither.onRight { + getTxHistoryItemsUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + refresh = true, + ) + } + } + private fun onCheckFeeUpdate() { val sendState = uiState.value.sendState ?: return val isSuccess = sendState.isSuccess From 5352dfcb6dba7f54e6c792432b71c56d922f8b26 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 17:45:09 +0300 Subject: [PATCH 77/87] Updated on 2026-08-14 --- .../extension/AppExtensionConfigurations.kt | 31 +++++-------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index f3d9c37c3e..457267d06d 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -1,7 +1,6 @@ package com.tangem.plugin.configuration.configurations.extension import com.android.build.gradle.AppExtension -import com.android.build.gradle.internal.dsl.DefaultConfig import com.tangem.plugin.configuration.model.AppConfig import com.tangem.plugin.configuration.model.BuildType import com.tangem.plugin.configuration.utils.BuildConfigFieldFactory @@ -10,15 +9,15 @@ import com.android.build.gradle.internal.dsl.BuildType as AndroidBuildType internal fun AppExtension.configure(project: Project) { configureCompileSdk() - val defaultConfig = configureDefaultConfig(project) + configureDefaultConfig(project) configureBuildFeatures() - configureBuildTypes(defaultConfig) + configureBuildTypes() configurePackagingOptions() configureCompose(project) configureCompilerOptions() } -private fun AppExtension.configureDefaultConfig(project: Project): DefaultConfig { +private fun AppExtension.configureDefaultConfig(project: Project) { defaultConfig { applicationId = AppConfig.packageName minSdk = AppConfig.minSdkVersion @@ -41,7 +40,6 @@ private fun AppExtension.configureDefaultConfig(project: Project): DefaultConfig testInstrumentationRunner = "com.tangem.common.HiltTestRunner" } - return defaultConfig } // TODO: [REDACTED_JIRA] @@ -51,14 +49,13 @@ private fun AppExtension.configureBuildFeatures() { } } -private fun AppExtension.configureBuildTypes(defaultConfig: DefaultConfig) { +private fun AppExtension.configureBuildTypes() { buildTypes { BuildType.values().forEach { buildType -> maybeCreate(buildType.id).apply { configureBuildVariant( appExtension = this@configureBuildTypes, buildType = buildType, - defaultConfig = defaultConfig ) BuildConfigFieldFactory( @@ -71,11 +68,7 @@ private fun AppExtension.configureBuildTypes(defaultConfig: DefaultConfig) { testBuildType = BuildType.Mocked.id } -private fun AndroidBuildType.configureBuildVariant( - appExtension: AppExtension, - buildType: BuildType, - defaultConfig: DefaultConfig, -) { +private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, buildType: BuildType) { when (buildType) { BuildType.Release -> { isDebuggable = false @@ -91,19 +84,13 @@ private fun AndroidBuildType.configureBuildVariant( matchingFallbacks.add(BuildType.Release.id) signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) } - BuildType.Internal -> { + BuildType.Internal, + BuildType.Mocked -> { initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) isDebuggable = true } - BuildType.Mocked -> { - defaultConfig.versionName = LARGE_VERSION_NAME - initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) - matchingFallbacks.add(BuildType.Mocked.id) - signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) - isDebuggable = true - } } versionNameSuffix = buildType.versionSuffix?.let { "-$it" } @@ -119,6 +106,4 @@ private fun AppExtension.configurePackagingOptions() { excludes += "META-INF/gradle/incremental.annotation.processors" } } -} - -private const val LARGE_VERSION_NAME = "100.0.0" \ No newline at end of file +} \ No newline at end of file From 1c737dda31ec0b4ce524607d8bf08c50e3edc985 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 19:47:18 +0500 Subject: [PATCH 78/87] Updated on 2026-08-14 --- .../QuotesUnsupportedCurrenciesIdAdapter.kt | 20 ++++++++++--------- .../tangem/blockchainsdk/utils/Blockchain.kt | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) 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 2a3ff567e6..c3600a3053 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 @@ -1,5 +1,7 @@ package com.tangem.data.tokens.utils +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.QuotesResponse /** @@ -48,16 +50,16 @@ internal class QuotesUnsupportedCurrenciesIdAdapter { /** * Map that contains unsupported currencies and their replacement for request */ + private val ethCoinId = Blockchain.Ethereum.toCoinId() private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = mapOf( - "optimistic-ethereum" to "ethereum", - "arbitrum-one" to "ethereum", - "zksync-ethereum" to "ethereum", - "manta-pacific" to "ethereum", - "polygon-zkevm-ethereum" to "ethereum", - "aurora-ethereum" to "ethereum", - "base-ethereum" to "ethereum", - "cyberconnect" to "ethereum", - "blast" to "ethereum", + Blockchain.Optimism.toCoinId() to ethCoinId, + Blockchain.Arbitrum.toCoinId() to ethCoinId, + Blockchain.ZkSyncEra.toCoinId() to ethCoinId, + Blockchain.Manta.toCoinId() to ethCoinId, + Blockchain.PolygonZkEVM.toCoinId() to ethCoinId, + Blockchain.Aurora.toCoinId() to ethCoinId, + Blockchain.Base.toCoinId() to ethCoinId, + Blockchain.Blast.toCoinId() to ethCoinId, ) } } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 58b9388a8e..8d53ec6fb6 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -327,7 +327,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Joystream -> "joystream" Blockchain.Bittensor -> "bittensor" Blockchain.Filecoin -> "filecoin" - Blockchain.Blast, Blockchain.BlastTestnet -> "blast" + Blockchain.Blast, Blockchain.BlastTestnet -> "blast-ethereum" Blockchain.Cyber, Blockchain.CyberTestnet -> "cyberconnect" } } From 16235188ede23a697a52216a4efe9beda13b2b2d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 20:04:38 +0500 Subject: [PATCH 79/87] Updated on 2026-08-14 --- .../presentation/viewmodel/SendViewModel.kt | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 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 68cbb4d8dc..67de572bab 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 @@ -34,9 +34,9 @@ import com.tangem.domain.transaction.error.ValidateAddressError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase -import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -57,10 +57,8 @@ import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal import java.util.Locale @@ -934,16 +932,24 @@ internal class SendViewModel @Inject constructor( private fun scheduleUpdates() { coroutineScope.launch { - // we should update network to find pending tx after 1 sec - fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrency.network)) - // we should update tx history and network for new balance - updateTxHistory() - updateDelayedCurrencyStatusUseCase( - userWalletId = userWallet.walletId, - network = cryptoCurrency.network, - delayMillis = BALANCE_UPDATE_DELAY, - refresh = true, - ) + listOf( + // we should update network to find pending tx after 1 sec + async { + fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrency.network)) + }, + // we should update tx history and network for new balance + async { + updateTxHistory() + }, + async { + updateDelayedCurrencyStatusUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + delayMillis = BALANCE_UPDATE_DELAY, + refresh = true, + ) + }, + ).awaitAll() } } From 3a8897cd3c0885a3b373c9f9a8e19dd3d586dfa4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 11:02:29 +0500 Subject: [PATCH 80/87] Updated on 2026-08-14 --- .../GiveTxPermisssionBottomSheet.kt | 48 ++++++++-------- .../permission/state/GiveTxPermissionState.kt | 7 ++- .../presentation/state/StakingNotification.kt | 5 ++ .../SetConfirmationStateAssentTransformer.kt | 10 ++-- ...etConfirmationStateCompletedTransformer.kt | 7 ++- .../SetApprovalInProgressTransformer.kt | 32 +++++++++++ ...firmationStateAssentApprovalTransformer.kt | 7 ++- .../ShowApprovalBottomSheetTransformer.kt | 9 ++- .../ui/block/NotificationsBlock.kt | 33 ++++++++--- .../viewmodel/StakingViewModel.kt | 57 +++++++++++++++---- 10 files changed, 158 insertions(+), 57 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalInProgressTransformer.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 6f61b650b4..73c62d311c 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -164,6 +164,7 @@ private fun AmountItem( .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) .clickable( + enabled = onChangeApproveType != null, interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), onClick = { isExpandSelector = true }, @@ -190,36 +191,35 @@ private fun AmountItem( ) SpacerWMax() Text( - text = stringResource( - when (approveType) { - ApproveType.LIMITED -> R.string.give_permission_current_transaction - ApproveType.UNLIMITED -> R.string.give_permission_unlimited - }, - ), + text = approveType.text.resolveReference(), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body1, maxLines = 1, ) - Icon( - painter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.ic_chevron_24)), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - modifier = Modifier.padding(start = TangemTheme.dimens.spacing2), + if (onChangeApproveType != null) { + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.ic_chevron_24)), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing2), + ) + } + } + if (onChangeApproveType != null) { + DropdownSelector( + isExpanded = isExpandSelector, + onDismiss = { isExpandSelector = false }, + onItemClick = { approveType -> + onChangeApproveType.let { + isExpandSelector = false + onChangeApproveType.invoke(approveType) + } + }, + items = approveItems, + selectedType = approveType, + amountSize = amountSize, ) } - DropdownSelector( - isExpanded = isExpandSelector, - onDismiss = { isExpandSelector = false }, - onItemClick = { approveType -> - onChangeApproveType?.let { - isExpandSelector = false - onChangeApproveType.invoke(approveType) - } - }, - items = approveItems, - selectedType = approveType, - amountSize = amountSize, - ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt index 908e42559b..9f52bca622 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt @@ -1,6 +1,8 @@ package com.tangem.common.ui.bottomsheet.permission.state +import com.tangem.common.ui.R import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -26,8 +28,9 @@ sealed class GiveTxPermissionState { ) : GiveTxPermissionState() } -enum class ApproveType { - LIMITED, UNLIMITED +enum class ApproveType(val text: TextReference) { + LIMITED(resourceReference(R.string.give_permission_current_transaction)), + UNLIMITED(resourceReference(R.string.give_permission_unlimited)), } data class ApprovePermissionButton( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt index 88936286d3..c338f2c047 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -69,5 +69,10 @@ internal sealed class StakingNotification(val config: NotificationConfig) { wrappedList(cooldownPeriodDays, cooldownPeriodDays), ), ) + + data class TransactionInProgress( + val title: TextReference, + val description: TextReference, + ) : Warning(title = title, subtitle = description) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index 33b475fcaf..ca470397b6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -6,7 +6,8 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.Provider @@ -15,7 +16,7 @@ import kotlinx.collections.immutable.ImmutableList internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val stakingGasEstimate: StakingGasEstimate, private val pendingActionList: ImmutableList, ) : Transformer { @@ -30,6 +31,7 @@ internal class SetConfirmationStateAssentTransformer( gasEstimate: StakingGasEstimate, ): StakingStates.ConfirmationState { if (this is StakingStates.ConfirmationState.Data) { + val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true return copy( innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Content( @@ -40,8 +42,8 @@ internal class SetConfirmationStateAssentTransformer( decimals = gasEstimate.token.decimals, ), ), - rate = cryptoCurrencyStatusProvider().value.fiatRate, - isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate, + rate = feeCryptoCurrencyStatus?.value?.fiatRate, + isFeeConvertibleToFiat = isFeeConvertibleToFiat, appCurrency = appCurrencyProvider(), isFeeApproximate = false, ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index fa8af0041e..6b11ad41ab 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -14,7 +14,7 @@ import com.tangem.utils.transformer.Transformer internal class SetConfirmationStateCompletedTransformer( private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val stakingGasEstimate: StakingGasEstimate, private val txUrl: String, ) : Transformer { @@ -29,6 +29,7 @@ internal class SetConfirmationStateCompletedTransformer( gasEstimate: StakingGasEstimate, ): StakingStates.ConfirmationState { if (this is StakingStates.ConfirmationState.Data) { + val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true return copy( isPrimaryButtonEnabled = true, innerState = InnerConfirmationStakingState.COMPLETED, @@ -40,8 +41,8 @@ internal class SetConfirmationStateCompletedTransformer( decimals = gasEstimate.token.decimals, ), ), - rate = cryptoCurrencyStatusProvider().value.fiatRate, - isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate, + rate = feeCryptoCurrencyStatus?.value?.fiatRate, + isFeeConvertibleToFiat = isFeeConvertibleToFiat, appCurrency = appCurrencyProvider(), isFeeApproximate = false, ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalInProgressTransformer.kt new file mode 100644 index 0000000000..f238a8c764 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalInProgressTransformer.kt @@ -0,0 +1,32 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.approval + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingNotification +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal object SetApprovalInProgressTransformer : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val state = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val notifications = state?.notifications?.toMutableList() ?: mutableListOf() + + // todo staking [REDACTED_TASK_KEY] replace with correct text + notifications.add( + StakingNotification.Warning.TransactionInProgress( + title = resourceReference(R.string.warning_express_approval_in_progress_title), + description = resourceReference(R.string.warning_express_approval_in_progress_message), + ), + ) + + val updatedConfirmationState = state?.copy( + notifications = notifications.toPersistentList(), + ) ?: prevState.confirmationState + + return prevState.copy( + confirmationState = updatedConfirmationState, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt index c3962befb2..939c6877dc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt @@ -12,7 +12,7 @@ import com.tangem.utils.transformer.Transformer internal class SetConfirmationStateAssentApprovalTransformer( private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val fee: TransactionFee, ) : Transformer { @@ -25,12 +25,13 @@ internal class SetConfirmationStateAssentApprovalTransformer( private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState { return if (this is StakingStates.ConfirmationState.Data) { + val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true copy( innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Content( fee = fee.normal, - rate = cryptoCurrencyStatusProvider().value.fiatRate, - isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate, + rate = feeCryptoCurrencyStatus?.value?.fiatRate, + isFeeConvertibleToFiat = isFeeConvertibleToFiat, appCurrency = appCurrencyProvider(), isFeeApproximate = false, ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt index 9eb4e68640..4c1c1647f9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt @@ -19,6 +19,7 @@ import com.tangem.utils.transformer.Transformer internal class ShowApprovalBottomSheetTransformer( private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val onDismiss: () -> Unit, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -29,15 +30,17 @@ internal class ShowApprovalBottomSheetTransformer( val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: return prevState val feeState = confirmationState.feeState as? FeeState.Content ?: return prevState + val fee = feeState.fee ?: return prevState val walletAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value.orEmpty() val validatorAddress = validatorState.chosenValidator.address val feeCryptoValue = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = feeState.fee?.amount?.value, - cryptoCurrency = cryptoCurrency, + cryptoAmount = fee.amount.value, + cryptoCurrency = fee.amount.currencySymbol, + decimals = fee.amount.decimals, ) val feeFiatValue = BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeState.fee?.amount?.maxValue, + fiatAmount = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value), fiatCurrencyCode = appCurrencyProvider().code, fiatCurrencySymbol = appCurrencyProvider().symbol, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt index a0c3d67869..a9dcad67cf 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt @@ -1,8 +1,13 @@ package com.tangem.features.staking.impl.presentation.ui.block +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.CardWithIcon import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.staking.impl.presentation.state.StakingNotification import kotlinx.collections.immutable.ImmutableList @@ -11,13 +16,27 @@ import kotlinx.collections.immutable.ImmutableList internal fun NotificationsBlock(notifications: ImmutableList) { notifications.forEach { notification -> key(notification) { - Notification( - config = notification.config, - iconTint = when (notification) { - is StakingNotification.Error -> TangemTheme.colors.icon.warning - is StakingNotification.Warning -> TangemTheme.colors.icon.accent - }, - ) + if (notification is StakingNotification.Warning.TransactionInProgress) { + CardWithIcon( + title = notification.title.resolveReference(), + description = notification.description.resolveReference(), + icon = { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size16), + color = TangemTheme.colors.icon.primary1, + strokeWidth = TangemTheme.dimens.size2, + ) + }, + ) + } else { + Notification( + config = notification.config, + iconTint = when (notification) { + is StakingNotification.Error -> TangemTheme.colors.icon.warning + is StakingNotification.Warning -> TangemTheme.colors.icon.accent + }, + ) + } } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index c4c0bac907..a9115c6ceb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -26,6 +26,7 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -45,13 +46,12 @@ import com.tangem.features.staking.impl.presentation.state.transformers.amount.A import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalInProgressTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.ShowApprovalBottomSheetTransformer import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.* @@ -68,6 +68,7 @@ internal class StakingViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getStakingTransactionUseCase: GetStakingTransactionUseCase, @@ -107,12 +108,14 @@ internal class StakingViewModel @Inject constructor( ?: error("This screen can't be opened without `Yield`") private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null private var innerRouter: InnerStakingRouter by Delegates.notNull() private var userWallet: UserWallet by Delegates.notNull() private var appCurrency: AppCurrency by Delegates.notNull() private var stakingApproval: StakingApproval = StakingApproval.Empty + private val allowanceTaskScheduler = SingleTaskScheduler() private var approvalJobHolder: JobHolder = JobHolder() @@ -271,7 +274,7 @@ internal class StakingViewModel @Inject constructor( stateController.update( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, stakingGasEstimate = stakingGasEstimate, pendingActionList = pendingActions, ), @@ -292,7 +295,7 @@ internal class StakingViewModel @Inject constructor( stateController.update( SetConfirmationStateAssentApprovalTransformer( appCurrencyProvider = Provider { appCurrency }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = approvalFee, ), ) @@ -354,6 +357,7 @@ internal class StakingViewModel @Inject constructor( ShowApprovalBottomSheetTransformer( appCurrencyProvider = Provider { appCurrency }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) { stateController.update(DismissBottomSheetStateTransformer()) }, @@ -391,7 +395,7 @@ internal class StakingViewModel @Inject constructor( stateController.update( SetConfirmationStateAssentApprovalTransformer( appCurrencyProvider = Provider { appCurrency }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = TransactionFee.Single(fee), ), ) @@ -411,20 +415,49 @@ internal class StakingViewModel @Inject constructor( stateController.update( SetConfirmationStateAssentApprovalTransformer( appCurrencyProvider = Provider { appCurrency }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = TransactionFee.Single(fee), ), ) // TODO staking error }, ifRight = { - getFee(confirmationState.pendingActions) + stateController.update(SetApprovalInProgressTransformer) stateController.update(DismissBottomSheetStateTransformer()) + awaitForAllowance(confirmationState.pendingActions) }, ) }.saveIn(approvalJobHolder) } + private fun awaitForAllowance(pendingActions: ImmutableList) { + val approval = stakingApproval as? StakingApproval.Needed ?: return + allowanceTaskScheduler.scheduleTask( + scope = viewModelScope, + task = PeriodicTask( + delay = ALLOWANCE_UPDATE_DELAY, + task = { + runCatching { + getAllowanceUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + spenderAddress = approval.spenderAddress, + ).getOrElse { BigDecimal.ZERO } + } + }, + onSuccess = { allowance -> + val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided") + if (allowance >= amount) { + getFee(pendingActions) + allowanceTaskScheduler.cancelTask() + } + }, + onError = { /* no-op */ }, + ), + ) + } + override fun onExploreClick() { val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content @@ -469,6 +502,7 @@ internal class StakingViewModel @Inject constructor( ) getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold( ifRight = { + feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, it).getOrNull() cryptoCurrencyStatus = it setupApprovalNeeded() @@ -531,7 +565,7 @@ internal class StakingViewModel @Inject constructor( stateController.update( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, stakingGasEstimate = gasEstimate, pendingActionList = pendingActionList, ), @@ -549,7 +583,7 @@ internal class StakingViewModel @Inject constructor( stateController.update( SetConfirmationStateCompletedTransformer( appCurrencyProvider = Provider { appCurrency }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, stakingGasEstimate = gasEstimate, txUrl = txUrl, ), @@ -592,7 +626,8 @@ internal class StakingViewModel @Inject constructor( InnerConfirmationStakingState.ASSENT } - companion object { + private companion object { const val WHAT_IS_STAKING_ARTICLE_URL = "TODO staking" + const val ALLOWANCE_UPDATE_DELAY = 10_000L } } \ No newline at end of file From 337b9908c4ce1bfced6b301b11d1b6b387c66739 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Aug 2024 10:49:59 +0400 Subject: [PATCH 81/87] Updated on 2026-08-14 --- .../domain/DefaultCustomTokenInteractor.kt | 2 +- .../note/redux/OnboardingNoteMiddleware.kt | 2 +- .../twins/redux/TwinCardsMiddleware.kt | 2 +- .../viewmodels/TokensListMigration.kt | 2 +- .../CryptoCurrencyConverter.kt | 2 +- .../tap/domain/card/CryptoCurrenciesMocks.kt | 2 +- data/common/build.gradle.kts | 19 ++++++++++--- .../common/currency}/CryptoCurrencyFactory.kt | 2 +- .../common/currency}/NetworkOperations.kt | 16 ++++++----- .../ResponseCryptoCurrenciesFactory.kt | 2 +- .../data/common/currency}/TokensOperations.kt | 27 +++++++++++-------- .../currency}/UserTokensResponseFactory.kt | 2 +- .../tokens/paging/CoinsResponseConverter.kt | 2 +- .../repository/DefaultCurrenciesRepository.kt | 5 +++- .../DefaultNetworksCompatibilityRepository.kt | 2 +- .../repository/DefaultNetworksRepository.kt | 2 +- .../utils/CardCryptoCurrenciesFactory.kt | 1 + features/referral/data/build.gradle.kts | 2 +- .../referral/data/ReferralRepositoryImpl.kt | 2 +- features/swap/data/build.gradle.kts | 2 +- .../feature/swap/DefaultSwapRepository.kt | 2 +- .../SavedSwapTransactionListConverter.kt | 4 +-- 22 files changed, 65 insertions(+), 39 deletions(-) rename data/{tokens/src/main/kotlin/com/tangem/data/tokens/utils => common/src/main/kotlin/com/tangem/data/common/currency}/CryptoCurrencyFactory.kt (99%) rename data/{tokens/src/main/kotlin/com/tangem/data/tokens/utils => common/src/main/kotlin/com/tangem/data/common/currency}/NetworkOperations.kt (85%) rename data/{tokens/src/main/kotlin/com/tangem/data/tokens/utils => common/src/main/kotlin/com/tangem/data/common/currency}/ResponseCryptoCurrenciesFactory.kt (99%) rename data/{tokens/src/main/kotlin/com/tangem/data/tokens/utils => common/src/main/kotlin/com/tangem/data/common/currency}/TokensOperations.kt (78%) rename data/{tokens/src/main/kotlin/com/tangem/data/tokens/utils => common/src/main/kotlin/com/tangem/data/common/currency}/UserTokensResponseFactory.kt (97%) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt index ad210d7625..bd9cc89301 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt @@ -4,7 +4,7 @@ import arrow.core.getOrElse import arrow.core.raise.result import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.features.addCustomToken.CustomCurrency 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 ef320383b2..52af2f7d66 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 @@ -5,7 +5,7 @@ import com.tangem.common.extensions.guard import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.derivationStyleProvider 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 19d77c509e..047a5caaa8 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 @@ -7,7 +7,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics -import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.derivationStyleProvider diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt index 41495924b3..5b6249b760 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.common.routing.AppRoute import com.tangem.common.routing.utils.popTo -import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt index 041b9561ee..d7a9569c4c 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt @@ -2,7 +2,7 @@ package com.tangem.tap.network.exchangeServices import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token -import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.extensions.inject diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt index 6cb075061a..5ecd65378b 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt @@ -2,7 +2,7 @@ package com.tangem.tap.domain.card import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token -import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts index f1a67e77a3..5a5b6c1d85 100644 --- a/data/common/build.gradle.kts +++ b/data/common/build.gradle.kts @@ -10,13 +10,26 @@ android { } dependencies { + /* Core */ implementation(projects.core.datasource) + /* Domain */ + implementation(projects.domain.models) + implementation(projects.domain.legacy) + implementation(projects.domain.tokens.models) + + /* Libs - SDK */ + implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) + implementation(projects.libs.blockchainSdk) + + /* DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /* Libs - Other */ implementation(deps.kotlin.coroutines) implementation(deps.jodatime) implementation(deps.timber) implementation(deps.arrow.core) - - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt similarity index 99% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt rename to data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt index b1e215ed19..9af9e67943 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt @@ -1,4 +1,4 @@ -package com.tangem.data.tokens.utils +package com.tangem.data.common.currency import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt similarity index 85% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt rename to data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt index c7986bd414..8f2e5b60ac 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt @@ -1,4 +1,4 @@ -package com.tangem.data.tokens.utils +package com.tangem.data.common.currency import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.FeePaidCurrency @@ -7,14 +7,14 @@ import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.tokens.model.Network import timber.log.Timber -internal fun getBlockchain(networkId: Network.ID): Blockchain { +fun getBlockchain(networkId: Network.ID): Blockchain { return Blockchain.fromId(networkId.value) } -internal fun getNetwork( +fun getNetwork( blockchain: Blockchain, extraDerivationPath: String?, - derivationStyleProvider: DerivationStyleProvider, + derivationStyleProvider: DerivationStyleProvider?, ): Network? { if (blockchain == Blockchain.Unknown) { Timber.e("Unable to convert Unknown blockchain to the domain network model") @@ -36,8 +36,12 @@ internal fun getNetwork( private fun getNetworkDerivationPath( blockchain: Blockchain, extraDerivationPath: String?, - cardDerivationStyleProvider: DerivationStyleProvider, + cardDerivationStyleProvider: DerivationStyleProvider?, ): Network.DerivationPath { + if (cardDerivationStyleProvider == null) { + return Network.DerivationPath.None + } + val defaultDerivationPath = getDefaultDerivationPath(blockchain, cardDerivationStyleProvider) return if (extraDerivationPath.isNullOrBlank()) { @@ -55,7 +59,7 @@ private fun getNetworkDerivationPath( } } -internal fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType { +fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType { return when (blockchain) { Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20 Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20 diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt similarity index 99% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt rename to data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt index 0ad1df2428..9bbdfb7fed 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt @@ -1,4 +1,4 @@ -package com.tangem.data.tokens.utils +package com.tangem.data.common.currency import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt similarity index 78% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt rename to data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt index a944703601..72018fc4ed 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt @@ -1,4 +1,4 @@ -package com.tangem.data.tokens.utils +package com.tangem.data.common.currency import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil @@ -18,30 +18,35 @@ private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws private const val TOKEN_ICON_SIZE = "large" private const val TOKEN_ICON_EXT = "png" -internal fun isCustomToken(tokenId: ID, network: Network): Boolean { +fun isCustomToken(tokenId: ID, network: Network): Boolean { return network.derivationPath is Network.DerivationPath.Custom || tokenId.rawCurrencyId == null } -internal fun isCustomCoin(network: Network): Boolean { +fun isCustomCoin(network: Network): Boolean { return network.derivationPath is Network.DerivationPath.Custom } -internal fun getCoinId(network: Network, coinId: String): ID { +fun getCoinId(network: Network, coinId: String): ID { return ID(COIN_ID_PREFIX, getCurrencyIdBody(network), CurrencyIdSuffix(rawId = coinId)) } -internal fun getTokenId(network: Network, sdkToken: SdkToken): ID { +fun getTokenId(network: Network, sdkToken: SdkToken): ID { val sdkTokenId = sdkToken.id - val suffix = if (sdkTokenId == null) { - CustomCurrencyIdSuffix(contractAddress = sdkToken.contractAddress) + + return getTokenId(network, sdkTokenId, sdkToken.contractAddress) +} + +fun getTokenId(network: Network, rawTokenId: String?, contractAddress: String): ID { + val suffix = if (rawTokenId == null) { + CustomCurrencyIdSuffix(contractAddress) } else { - CurrencyIdSuffix(rawId = sdkTokenId, contractAddress = sdkToken.contractAddress) + CurrencyIdSuffix(rawTokenId, contractAddress) } return ID(TOKEN_ID_PREFIX, getCurrencyIdBody(network), suffix) } -internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { +fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { val tokenId = token.id return if (tokenId == null) { @@ -51,7 +56,7 @@ internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { } } -internal fun getCoinIconUrl(blockchain: Blockchain): String? { +fun getCoinIconUrl(blockchain: Blockchain): String? { val coinId = when (blockchain) { Blockchain.Unknown -> null else -> blockchain.toCoinId() @@ -60,7 +65,7 @@ internal fun getCoinIconUrl(blockchain: Blockchain): String? { return coinId?.let(::getTokenIconUrlFromDefaultHost) } -internal fun List.hasCoinForToken(token: CryptoCurrency.Token): Boolean { +fun List.hasCoinForToken(token: CryptoCurrency.Token): Boolean { return any { val blockchain = getBlockchain(networkId = token.network.id) val tokenDerivation = token.network.derivationPath.value diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt similarity index 97% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt rename to data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt index 7e8372da6c..4ba8bbd2a2 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt @@ -1,4 +1,4 @@ -package com.tangem.data.tokens.utils +package com.tangem.data.common.currency import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt index a1a8018cab..611ccafe2b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt @@ -2,7 +2,7 @@ package com.tangem.data.tokens.paging import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.data.tokens.utils.getNetworkStandardType +import com.tangem.data.common.currency.getNetworkStandardType import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Token diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 238ab2676a..ec3720fe1b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -6,7 +6,10 @@ import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.tokens.utils.* +import com.tangem.data.common.currency.* +import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory +import com.tangem.data.tokens.utils.CustomTokensMerger +import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi 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 02fdef2739..c0ea21218c 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 @@ -3,7 +3,7 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.card.EllipticCurve -import com.tangem.data.tokens.utils.getNetwork +import com.tangem.data.common.currency.getNetwork import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.extensions.* diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 58aa0a6dd6..de6ad111ab 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -5,9 +5,9 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory -import com.tangem.data.tokens.utils.ResponseCryptoCurrenciesFactory import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt index a2993452d9..4d6056cbb0 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt @@ -1,6 +1,7 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain +import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider diff --git a/features/referral/data/build.gradle.kts b/features/referral/data/build.gradle.kts index 5ca72579ad..954bac9c2e 100644 --- a/features/referral/data/build.gradle.kts +++ b/features/referral/data/build.gradle.kts @@ -17,7 +17,7 @@ dependencies { implementation(projects.core.utils) /** Data modules */ - implementation(projects.data.tokens) + implementation(projects.data.common) /** Domain modules */ implementation(projects.domain.legacy) diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 2d44c137ff..9a901c1b69 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -3,7 +3,7 @@ package com.tangem.feature.referral.data import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.StartReferralBody diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 5998ddafd0..f9d6ff0013 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -37,7 +37,7 @@ dependencies { implementation(projects.domain.wallets.models) /** Data */ - implementation(projects.data.tokens) + implementation(projects.data.common) /** Tangem SDKs */ implementation(deps.tangem.blockchain) 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 48724f9b77..31b1659a73 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 @@ -8,7 +8,7 @@ import arrow.core.right import com.squareup.moshi.Moshi import com.tangem.blockchain.common.* import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt index 8e41921960..33112a9591 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.converters -import com.tangem.data.tokens.utils.ResponseCryptoCurrenciesFactory -import com.tangem.data.tokens.utils.UserTokensResponseFactory +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId From 292d895c4a74709d54963fb8c515c3d5b8bdf4cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 13:22:59 +0400 Subject: [PATCH 82/87] Updated on 2026-08-14 --- .../kotlin/com/tangem/data/common}/utils/RequestUtils.kt | 9 ++++++--- .../tangem/data/markets/DefaultMarketsTokenRepository.kt | 7 +++++-- .../com/tangem/data/markets/MarketsBatchUpdateFetcher.kt | 6 ++++-- 3 files changed, 15 insertions(+), 7 deletions(-) rename data/{markets/src/main/java/com/tangem/data/markets => common/src/main/kotlin/com/tangem/data/common}/utils/RequestUtils.kt (77%) diff --git a/data/markets/src/main/java/com/tangem/data/markets/utils/RequestUtils.kt b/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt similarity index 77% rename from data/markets/src/main/java/com/tangem/data/markets/utils/RequestUtils.kt rename to data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt index 18f4e1fda0..e7f3fbfe45 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/utils/RequestUtils.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt @@ -1,4 +1,4 @@ -package com.tangem.data.markets.utils +package com.tangem.data.common.utils import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay @@ -8,7 +8,7 @@ import timber.log.Timber import kotlin.coroutines.cancellation.CancellationException @Suppress("UnconditionalJumpStatementInLoop") -internal suspend fun retryOnError(priority: Boolean = false, call: suspend () -> T): T { +suspend fun retryOnError(priority: Boolean = false, call: suspend () -> T): T { while (true) { return try { call() @@ -16,11 +16,14 @@ internal suspend fun retryOnError(priority: Boolean = false, call: suspend ( if (e is CancellationException) { currentCoroutineContext().ensureActive() } - Timber.e(e) + + Timber.e(e, "Error occurred during retryOnError block") + if (priority.not()) { yield() delay(timeMillis = 500) } + continue } } diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index e850d1ed35..de9edf3b4b 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -1,7 +1,10 @@ package com.tangem.data.markets -import com.tangem.data.markets.converters.* -import com.tangem.data.markets.utils.retryOnError +import com.tangem.data.common.utils.retryOnError +import com.tangem.data.markets.converters.TokenChartConverter +import com.tangem.data.markets.converters.TokenMarketInfoConverter +import com.tangem.data.markets.converters.TokenMarketListConverter +import com.tangem.data.markets.converters.toRequestParam import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.tangemTech.TangemTechApi diff --git a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt index d1db77b24e..3a377ab901 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt @@ -1,9 +1,9 @@ package com.tangem.data.markets +import com.tangem.data.common.utils.retryOnError import com.tangem.data.markets.converters.TokenMarketChartsConverter import com.tangem.data.markets.converters.TokenQuotesShortConverter import com.tangem.data.markets.converters.toRequestParam -import com.tangem.data.markets.utils.retryOnError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse @@ -13,7 +13,9 @@ import com.tangem.domain.markets.TokenMarketUpdateRequest import com.tangem.pagination.Batch import com.tangem.pagination.BatchUpdateFetcher import com.tangem.pagination.BatchUpdateResult -import kotlinx.coroutines.* +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch internal class MarketsBatchUpdateFetcher( private val marketsApi: TangemTechMarketsApi, From 8dc8119970253bf86dded5fe72f64fb088c8617a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 13:25:20 +0400 Subject: [PATCH 83/87] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + data/manage-tokens/.gitignore | 1 + data/manage-tokens/build.gradle.kts | 44 +++++ .../DefaultManageTokensRepository.kt | 138 ++++++++++++++ .../managetokens/di/ManageTokensDataModule.kt | 40 +++++ .../utils/ManageTokensUpdateFetcher.kt | 68 +++++++ .../utils/ManagedCryptoCurrencyFactory.kt | 168 ++++++++++++++++++ settings.gradle.kts | 1 + 8 files changed, 461 insertions(+) create mode 100644 data/manage-tokens/.gitignore create mode 100644 data/manage-tokens/build.gradle.kts create mode 100644 data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt create mode 100644 data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt create mode 100644 data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt create mode 100644 data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 968acfe32e..01c88fedd1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -104,6 +104,7 @@ dependencies { implementation(projects.data.staking) implementation(projects.data.walletConnect) implementation(projects.data.markets) + implementation(projects.data.manageTokens) /** Features */ implementation(projects.features.onboarding) diff --git a/data/manage-tokens/.gitignore b/data/manage-tokens/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/manage-tokens/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/manage-tokens/build.gradle.kts b/data/manage-tokens/build.gradle.kts new file mode 100644 index 0000000000..137a893c81 --- /dev/null +++ b/data/manage-tokens/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.example.data.managetokens" +} + +dependencies { + + /** Project - Domain */ + implementation(projects.domain.models) + implementation(projects.domain.manageTokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + + /** Project - Data */ + implementation(projects.core.datasource) + implementation(projects.data.common) + + /** Project - Utils */ + implementation(projects.core.utils) + implementation(projects.core.pagination) + implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) + + /** Tangem SDKs */ + implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) + + /** AndroidX */ + implementation(deps.androidx.datastore) + + /** DI */ + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + + /** Other */ + implementation(deps.moshi.kotlin) + implementation(deps.timber) +} \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt new file mode 100644 index 0000000000..bb45239024 --- /dev/null +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -0,0 +1,138 @@ +package com.tangem.data.managetokens + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.isSupportedInApp +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.data.common.utils.retryOnError +import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher +import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.supportedBlockchains +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.managetokens.model.ManageTokensListBatchFlow +import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext +import com.tangem.domain.managetokens.model.ManageTokensListConfig +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.repository.ManageTokensRepository +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.BatchListSource +import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher +import com.tangem.pagination.toBatchFlow +import com.tangem.utils.coroutines.CoroutineDispatcherProvider + +internal class DefaultManageTokensRepository( + private val tangemTechApi: TangemTechApi, + private val userWalletsStore: UserWalletsStore, + private val managedCryptoCurrencyFactory: ManagedCryptoCurrencyFactory, + private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : ManageTokensRepository { + + override fun getTokenListBatchFlow( + context: ManageTokensListBatchingContext, + batchSize: Int, + ): ManageTokensListBatchFlow { + return BatchListSource( + fetchDispatcher = dispatchers.io, + context = context, + generateNewKey = { it.size.inc() }, + batchFetcher = createFetcher(batchSize), + updateFetcher = manageTokensUpdateFetcher, + ).toBatchFlow() + } + + @Suppress("ComplexCondition") + private fun createFetcher( + batchSize: Int, + ): LimitOffsetBatchFetcher> = LimitOffsetBatchFetcher( + prefetchDistance = batchSize, + batchSize = batchSize, + subFetcher = { request, _, isFirstBatchFetching -> + val userWallet = getUserWallet(request.params.userWalletId) + val supportedBlockchains = getSupportedBlockchains(userWallet) + val searchText = request.params.searchText?.takeIf { it.isNotBlank() } + + val call = suspend { + tangemTechApi.getCoins( + networkIds = supportedBlockchains.joinToString( + separator = ",", + transform = Blockchain::toNetworkId, + ), + active = true, + searchText = searchText, + offset = request.offset * request.limit, + limit = request.limit, + ).getOrThrow() + } + + val coinsResponse = if (isFirstBatchFetching) { + call() + } else { + retryOnError(call = call) + } + + val tokensResponse = getStoredUserTokens(request.params.userWalletId) + val items = if (isFirstBatchFetching && + tokensResponse != null && + userWallet != null && + request.params.searchText.isNullOrBlank() + ) { + managedCryptoCurrencyFactory.createWithCustomTokens( + coinsResponse = coinsResponse, + tokensResponse = tokensResponse, + derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, + ) + } else { + managedCryptoCurrencyFactory.create( + coinsResponse = coinsResponse, + tokensResponse = tokensResponse, + derivationStyleProvider = userWallet?.scanResponse?.derivationStyleProvider, + ) + } + + BatchFetchResult.Success( + data = items, + empty = items.isEmpty(), + last = items.size < request.limit, + ) + }, + ) + + private suspend fun getStoredUserTokens(userWalletId: UserWalletId?): UserTokensResponse? { + return if (userWalletId != null) { + appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), + ) + } else { + null + } + } + + private suspend fun getUserWallet(userWalletId: UserWalletId?): UserWallet? { + if (userWalletId == null) { + return null + } + + return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "User wallet not found" + } + } + + private fun getSupportedBlockchains(userWallet: UserWallet?): List { + return userWallet?.scanResponse?.let { + it.card.supportedBlockchains(it.cardTypesResolver) + } ?: Blockchain.entries.filter { + !it.isTestnet() && it.isSupportedInApp() + } + } +} \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt new file mode 100644 index 0000000000..5cb10e4936 --- /dev/null +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -0,0 +1,40 @@ +package com.tangem.data.managetokens.di + +import com.tangem.data.managetokens.DefaultManageTokensRepository +import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher +import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.managetokens.repository.ManageTokensRepository +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 ManageTokensDataModule { + + @Provides + @Singleton + fun provideManageTokensRepository( + tangemTechApi: TangemTechApi, + userWalletsStore: UserWalletsStore, + managedCryptoCurrencyFactory: ManagedCryptoCurrencyFactory, + manageTokensUpdateFetcher: ManageTokensUpdateFetcher, + appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, + ): ManageTokensRepository { + return DefaultManageTokensRepository( + tangemTechApi, + userWalletsStore, + managedCryptoCurrencyFactory, + manageTokensUpdateFetcher, + appPreferencesStore, + dispatchers, + ) + } +} \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt new file mode 100644 index 0000000000..6c23f37b0b --- /dev/null +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt @@ -0,0 +1,68 @@ +package com.tangem.data.managetokens.utils + +import com.tangem.domain.managetokens.model.ManageTokensUpdateAction +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.pagination.Batch +import com.tangem.pagination.BatchUpdateFetcher +import com.tangem.pagination.BatchUpdateResult +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class ManageTokensUpdateFetcher @Inject constructor() : + BatchUpdateFetcher, ManageTokensUpdateAction> { + + override suspend fun BatchUpdateFetcher.UpdateContext>.fetchUpdateAsync( + toUpdate: List>>, + updateRequest: ManageTokensUpdateAction, + ) { + when (updateRequest) { + is ManageTokensUpdateAction.AddCurrency -> coroutineScope { + val tasks = toUpdate.map { (key, data) -> + async { + val currencyIndex = data.indexOfFirst { it.id == updateRequest.currencyId } + .takeIf { it != -1 } + ?: error("Currency '${updateRequest.currencyId}' not found in batch #$key") + val updatedCurrency = when (val currency = data[currencyIndex]) { + is ManagedCryptoCurrency.Custom -> error("Can't add custom currency '${currency.id}'") + is ManagedCryptoCurrency.Token -> { + currency.copy( + addedIn = if (updateRequest.isSelected) { + currency.addedIn + updateRequest.networkId + } else { + currency.addedIn - updateRequest.networkId + }, + ) + } + } + + data.toMutableList().apply { + set(currencyIndex, updatedCurrency) + } + } + } + + tasks.forEachIndexed { index, task -> + launch { + val updatedItems = task.await() + + update { + BatchUpdateResult.Success( + data = mapNotNull { + if (it.key == toUpdate[index].key) { + Batch(it.key, updatedItems) + } else { + null + } + }, + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt new file mode 100644 index 0000000000..de239a1f09 --- /dev/null +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -0,0 +1,168 @@ +package com.tangem.data.managetokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.isSupportedInApp +import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.data.common.currency.getCoinId +import com.tangem.data.common.currency.getNetwork +import com.tangem.data.common.currency.getTokenId +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.common.DerivationStyleProvider +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork +import com.tangem.domain.tokens.model.Network +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class ManagedCryptoCurrencyFactory @Inject constructor() { + + fun create( + coinsResponse: CoinsResponse, + tokensResponse: UserTokensResponse?, + derivationStyleProvider: DerivationStyleProvider?, + ): List { + return coinsResponse.coins.mapNotNull { coin -> + createToken(coin, tokensResponse, coinsResponse.imageHost, derivationStyleProvider) + } + } + + fun createWithCustomTokens( + coinsResponse: CoinsResponse, + tokensResponse: UserTokensResponse, + derivationStyleProvider: DerivationStyleProvider, + ): List { + val customTokens = tokensResponse.tokens + .mapNotNull { token -> + maybeCreateCustomToken(token, coinsResponse.imageHost, derivationStyleProvider) + } + + val tokens = create(coinsResponse, tokensResponse, derivationStyleProvider) + + return customTokens + tokens + } + + private fun maybeCreateCustomToken( + token: UserTokensResponse.Token, + imageHost: String?, + derivationStyleProvider: DerivationStyleProvider, + ): ManagedCryptoCurrency? { + val blockchain = Blockchain.fromNetworkId(token.networkId) + ?.takeIf { it.isSupportedInApp() } + ?: return null + + if (!checkIsCustomToken(token, blockchain, derivationStyleProvider)) { + return null + } + + val network = getNetwork( + blockchain = blockchain, + extraDerivationPath = token.derivationPath, + derivationStyleProvider = derivationStyleProvider, + ) ?: return null + val contractAddress = token.contractAddress + + return if (contractAddress.isNullOrBlank()) { + ManagedCryptoCurrency.Custom.Coin( + currencyId = getCoinId(network, blockchain.toCoinId()), + name = token.name, + symbol = token.symbol, + iconUrl = getIconUrl(blockchain.id, imageHost), + network = network, + ) + } else { + ManagedCryptoCurrency.Custom.Token( + currencyId = getTokenId(network, token.id, contractAddress), + name = token.name, + symbol = token.symbol, + iconUrl = token.id?.let { getIconUrl(it, imageHost) }, + contractAddress = contractAddress, + network = network, + ) + } + } + + private fun createToken( + coinResponse: CoinsResponse.Coin, + tokensResponse: UserTokensResponse?, + imageHost: String?, + derivationStyleProvider: DerivationStyleProvider?, + ): ManagedCryptoCurrency? { + if (coinResponse.networks.isEmpty() || !coinResponse.active) return null + + return ManagedCryptoCurrency.Token( + id = ManagedCryptoCurrency.ID(coinResponse.id), + name = coinResponse.name, + symbol = coinResponse.symbol, + iconUrl = getIconUrl(coinResponse.id, imageHost), + availableNetworks = coinResponse.networks.mapNotNull { network -> + createSource(network, derivationStyleProvider) + }, + addedIn = findAddedInNetworksIds(coinResponse.id, tokensResponse), + ) + } + + private fun createSource( + networkResponse: CoinsResponse.Coin.Network, + derivationStyleProvider: DerivationStyleProvider?, + extraDerivationPath: String? = null, + ): SourceNetwork? { + val blockchain = Blockchain.fromNetworkId(networkResponse.networkId) + ?.takeIf { it.isSupportedInApp() } + ?: return null + + val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null + val contractAddress = networkResponse.contractAddress + + return if (contractAddress.isNullOrBlank()) { + SourceNetwork.Main( + network = network, + ) + } else { + SourceNetwork.Default( + network = network, + contractAddress = contractAddress, + ) + } + } + + private fun findAddedInNetworksIds(currencyId: String, tokensResponse: UserTokensResponse?): Set { + if (tokensResponse == null) return emptySet() + + return tokensResponse.tokens + .filter { it.id == currencyId } + .map { it.networkId } + .mapNotNullTo(mutableSetOf()) { networkId -> + val blockchain = Blockchain.fromNetworkId(networkId) + + if (blockchain != null && blockchain.isSupportedInApp()) { + Network.ID(blockchain.id) + } else { + null + } + } + } + + private fun getIconUrl(id: String, imageHost: String?): String { + return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png" + } + + private fun checkIsCustomToken( + token: UserTokensResponse.Token, + blockchain: Blockchain, + derivationStyleProvider: DerivationStyleProvider, + ): Boolean = token.id.isNullOrBlank() || + checkIsCustomDerivationPath(token.derivationPath, blockchain, derivationStyleProvider) + + private fun checkIsCustomDerivationPath( + derivationPath: String?, + blockchain: Blockchain, + derivationStyleProvider: DerivationStyleProvider, + ): Boolean = derivationPath != blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath + + private companion object { + const val DEFAULT_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/" + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index a4720852bf..4af0388ca6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -250,4 +250,5 @@ include(":data:qr-scanning") include(":data:staking") include(":data:wallet-connect") include(":data:markets") +include(":data:manage-tokens") // endregion Data modules \ No newline at end of file From 70e40cb2a723689817504b974a59c1927ee1127f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Aug 2024 15:54:35 +0300 Subject: [PATCH 84/87] Updated on 2026-08-14 --- .../configurations/extension/AppExtensionConfigurations.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index 457267d06d..58ea63d4a6 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -79,12 +79,13 @@ private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, b isDebuggable = true isMinifyEnabled = false } - BuildType.External -> { + BuildType.Internal, + BuildType.External, + -> { initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) } - BuildType.Internal, BuildType.Mocked -> { initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) From bbb92804136d4fcd6014e80c771f67c8ff859774 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 12:42:11 +0300 Subject: [PATCH 85/87] Updated on 2026-08-14 --- .../tangem/tap/features/main/MainViewModel.kt | 6 +- .../com/tangem/datasource/di/NetworkModule.kt | 17 +- .../datasource/local/logs/AppLogsStore.kt | 69 ++++++ .../utils/NetworkLogsSaveInterceptor.kt | 197 ++++++++++++++++++ .../settings/DefaultSettingsRepository.kt | 42 +--- .../data/settings/di/SettingsDataModule.kt | 6 +- .../settings/DeleteDeprecatedLogsUseCase.kt | 2 +- .../repositories/SettingsRepository.kt | 4 +- 8 files changed, 294 insertions(+), 49 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt 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 c108d54aab..375a326b2b 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 @@ -37,7 +37,7 @@ internal class MainViewModel @Inject constructor( private val listenToFlipsUseCase: ListenToFlipsUseCase, private val router: AppRouter, private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase, - private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase, + deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase, private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase, private val blockchainSDKFactory: BlockchainSDKFactory, private val userWalletsListManager: UserWalletsListManager, @@ -76,9 +76,7 @@ internal class MainViewModel @Inject constructor( fetchStakingTokens() } - viewModelScope.launch(dispatchers.main) { - deleteDeprecatedLogsUseCase() - } + deleteDeprecatedLogsUseCase() } /** Loading the resources needed to run the application */ diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index b43900233f..c692a451c2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -14,6 +14,7 @@ import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApiV2 +import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.utils.* import com.tangem.datasource.utils.RequestHeader.AppVersionPlatformHeaders @@ -58,12 +59,18 @@ internal object NetworkModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, apiConfigsManager: ApiConfigsManager, + appLogsStore: AppLogsStore, ): TangemExpressApi { return createApi( id = ApiConfig.ID.Express, moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + clientBuilder = { + addInterceptor( + NetworkLogsSaveInterceptor(appLogsStore), + ) + }, ) } @@ -73,12 +80,18 @@ internal object NetworkModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, apiConfigsManager: ApiConfigsManager, + appLogsStore: AppLogsStore, ): StakeKitApi { return createApi( id = ApiConfig.ID.StakeKit, moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + clientBuilder = { + addInterceptor( + NetworkLogsSaveInterceptor(appLogsStore), + ) + }, ) } @@ -94,6 +107,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + clientBuilder = { applyTimeoutAnnotations() }, ) } @@ -200,6 +214,7 @@ internal object NetworkModule { moshi: Moshi, context: Context, apiConfigsManager: ApiConfigsManager, + clientBuilder: OkHttpClient.Builder.() -> OkHttpClient.Builder = { this }, ): T { val environmentConfig = apiConfigsManager.getEnvironmentConfig(id) @@ -210,8 +225,8 @@ internal object NetworkModule { .client( OkHttpClient.Builder() .applyApiConfig(id, apiConfigsManager) - .applyTimeoutAnnotations() .addLoggers(context) + .clientBuilder() .build(), ) .build() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt new file mode 100644 index 0000000000..38a05e8d79 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt @@ -0,0 +1,69 @@ +package com.tangem.datasource.local.logs + +import androidx.datastore.preferences.core.MutablePreferences +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.joda.time.DateTime +import javax.inject.Inject + +/** + * Store for saving app logs + * + * @property appPreferencesStore app preferences store + * @param dispatchers coroutine dispatcher provider + * +[REDACTED_AUTHOR] + */ +class AppLogsStore @Inject constructor( + private val appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, +) { + + private val scope = CoroutineScope(dispatchers.io) + private val mutex = Mutex() + + /** Save log [message] */ + fun saveLogMessage(message: String) { + val newLogs = DateTime.now().millis.toString() to message + + appPreferencesStore.editDataWithLock { preferences -> + val savedLogs = preferences.getObjectMap(PreferencesKeys.APP_LOGS_KEY) + + preferences.setObjectMap(key = PreferencesKeys.APP_LOGS_KEY, value = savedLogs + newLogs) + } + } + + /** Delete deprecated logs if file size exceeds [maxSize] */ + fun deleteDeprecatedLogs(maxSize: Int) { + appPreferencesStore.editDataWithLock { preferences -> + val savedLogs = preferences.getObjectMap(PreferencesKeys.APP_LOGS_KEY) + + var sum = 0 + preferences.setObjectMap( + key = PreferencesKeys.APP_LOGS_KEY, + value = savedLogs.entries + .sortedBy(Map.Entry::key) + .takeLastWhile { + sum += it.value.length + sum < maxSize + } + .associate { it.key to it.value }, + ) + } + } + + private fun AppPreferencesStore.editDataWithLock( + transform: suspend AppPreferencesStore.(MutablePreferences) -> Unit, + ) { + scope.launch { + mutex.withLock { + editData(transform) + } + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt new file mode 100644 index 0000000000..ee74b94ab5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt @@ -0,0 +1,197 @@ +package com.tangem.datasource.utils + +import com.tangem.datasource.local.logs.AppLogsStore +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import okhttp3.internal.http.promisesBody +import okio.Buffer +import okio.EOFException +import okio.GzipSource +import okio.IOException +import org.json.JSONArray +import org.json.JSONObject +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.util.concurrent.TimeUnit + +private const val JSON_INDENT_SPACES = 4 + +/** + * Interceptor for save network requests and responses logs + * + * @property appLogsStore app logs store + * +[REDACTED_AUTHOR] + */ +internal class NetworkLogsSaveInterceptor( + private val appLogsStore: AppLogsStore, +) : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + + logRequestMessage(chain, request) + + val startNs = System.nanoTime() + val response: Response + try { + response = chain.proceed(request) + } catch (e: Exception) { + appLogsStore.saveLogMessage("<-- HTTP FAILED: $e") + throw e + } + + logResponseMessage(response, startNs) + + return response + } + + private fun logRequestMessage(chain: Interceptor.Chain, request: Request) { + val connection = chain.connection() + val connectionProtocol = if (connection != null) " ${connection.protocol()}" else "" + + appLogsStore.saveLogMessage( + "--> ${request.method} ${request.url}$connectionProtocol\n" + + createRequestEndMessage(request), + ) + } + + private fun createRequestEndMessage(request: Request): String { + val requestBody = request.body + val method = request.method + + return if (requestBody == null) { + "--> END $method" + } else if (bodyHasUnknownEncoding(request.headers)) { + "--> END $method (encoded body omitted)" + } else if (requestBody.isDuplex()) { + "--> END $method (duplex request body omitted)" + } else if (requestBody.isOneShot()) { + "--> END $method (one-shot body omitted)" + } else { + val buffer = Buffer() + requestBody.writeTo(buffer) + + val contentType = requestBody.contentType() + val charset: Charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8 + + if (buffer.isProbablyUtf8()) { + val json = buffer.readString(charset).beautifyJson() + "$json\n--> END $method (${requestBody.contentLength()}-byte body)" + } else { + "--> END $method (binary ${requestBody.contentLength()}-byte body omitted)" + } + } + } + + private fun logResponseMessage(response: Response, startNs: Long) { + val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) + + val responseMessage = if (response.message.isEmpty()) "" else ' ' + response.message + val startMessage = "<-- ${response.code}$responseMessage ${response.request.url} " + + "(${tookMs}ms)" + + val responseHeaders = response.headers + val responseBody = response.body!! + val contentLength = responseBody.contentLength() + + val message = if (!response.promisesBody()) { + "<-- END HTTP" + } else if (bodyHasUnknownEncoding(response.headers)) { + "<-- END HTTP (encoded body omitted)" + } else { + val source = responseBody.source() + source.request(Long.MAX_VALUE) + var buffer = source.buffer + + var gzippedLength: Long? = null + if ("gzip".equals(responseHeaders["Content-Encoding"], ignoreCase = true)) { + gzippedLength = buffer.size + GzipSource(buffer.clone()).use { gzippedResponseBody -> + buffer = Buffer() + buffer.writeAll(gzippedResponseBody) + } + } + + val contentType = responseBody.contentType() + val charset: Charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8 + + if (!buffer.isProbablyUtf8()) { + "<-- END HTTP (binary ${buffer.size}-byte body omitted)" + } else { + val json = if (contentLength != 0L) { + buffer.clone().readString(charset).beautifyJson() + } else { + "" + } + + val end = if (gzippedLength != null) { + "<-- END HTTP (${buffer.size}-byte, $gzippedLength-gzipped-byte body)" + } else { + "<-- END HTTP (${buffer.size}-byte body)" + } + + "$json\n$end" + } + } + + appLogsStore.saveLogMessage(startMessage + "\n" + message) + } + + private fun bodyHasUnknownEncoding(headers: Headers): Boolean { + val contentEncoding = headers["Content-Encoding"] ?: return false + return !contentEncoding.equals("identity", ignoreCase = true) && + !contentEncoding.equals("gzip", ignoreCase = true) + } + + private fun Buffer.isProbablyUtf8(): Boolean { + try { + val prefix = Buffer() + val byteCount = size.coerceAtMost(maximumValue = 64) + copyTo(out = prefix, offset = 0, byteCount = byteCount) + + @Suppress("MagicNumber", "UnusedPrivateMember") + for (i in 0 until 16) { + if (prefix.exhausted()) break + + val codePoint = prefix.readUtf8CodePoint() + if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) return false + } + + return true + } catch (_: EOFException) { + return false + } + } + + private fun String.beautifyJson(): String { + beautifyIfObject(json = this)?.let { + return it + } + + beautifyIfArray(json = this)?.let { + return it + } + + return this + } + + private fun beautifyIfObject(json: String): String? { + return try { + JSONObject(json).toString(JSON_INDENT_SPACES) + } catch (e: Exception) { + null + } + } + + private fun beautifyIfArray(json: String): String? { + return try { + JSONArray(json).toString(JSON_INDENT_SPACES) + } catch (e: Exception) { + null + } + } +} \ 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 fd7a139ab2..6ff9910d3e 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 @@ -1,25 +1,17 @@ package com.tangem.data.settings +import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import org.joda.time.DateTime internal class DefaultSettingsRepository( private val appPreferencesStore: AppPreferencesStore, - dispatchers: CoroutineDispatcherProvider, + private val appLogsStore: AppLogsStore, ) : SettingsRepository { - private val scope = CoroutineScope(dispatchers.io) - private val mutex = Mutex() - override suspend fun shouldShowSaveUserWalletScreen(): Boolean { return appPreferencesStore.getSyncOrDefault( key = PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY, @@ -46,35 +38,11 @@ internal class DefaultSettingsRepository( } override fun saveLogMessage(message: String) { - val newLogs = DateTime.now().millis.toString() to message - - scope.launch { - mutex.withLock { - appPreferencesStore.editData { preferences -> - val savedLogs = preferences.getObjectMap(PreferencesKeys.APP_LOGS_KEY) - - preferences.setObjectMap(key = PreferencesKeys.APP_LOGS_KEY, value = savedLogs + newLogs) - } - } - } + appLogsStore.saveLogMessage(message) } - 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.entries - .sortedBy(Map.Entry::key) - .takeLastWhile { - sum += it.value.length - sum < maxSize - } - .associate { it.key to it.value }, - ) - } + override fun deleteDeprecatedLogs(maxSize: Int) { + appLogsStore.deleteDeprecatedLogs(maxSize) } override suspend fun isSendTapHelpPreviewEnabled(): Boolean { diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index 3efdd2e7eb..bddc8ef6e1 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -4,12 +4,12 @@ import com.tangem.data.settings.DefaultAppRatingRepository import com.tangem.data.settings.DefaultPermissionRepository import com.tangem.data.settings.DefaultPromoSettingsRepository import com.tangem.data.settings.DefaultSettingsRepository +import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.PermissionRepository import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -24,9 +24,9 @@ internal object SettingsDataModule { @Singleton fun provideSettingsRepository( appPreferencesStore: AppPreferencesStore, - dispatchers: CoroutineDispatcherProvider, + appLogsStore: AppLogsStore, ): SettingsRepository { - return DefaultSettingsRepository(appPreferencesStore = appPreferencesStore, dispatchers = dispatchers) + return DefaultSettingsRepository(appPreferencesStore = appPreferencesStore, appLogsStore = appLogsStore) } @Provides 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 index 627d3f1124..bff7acf23a 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/DeleteDeprecatedLogsUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/DeleteDeprecatedLogsUseCase.kt @@ -6,7 +6,7 @@ class DeleteDeprecatedLogsUseCase( private val settingsRepository: SettingsRepository, ) { - suspend operator fun invoke() { + operator fun invoke() { settingsRepository.deleteDeprecatedLogs(maxSize = MAX_LOGS_SIZE) } 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 992990fe85..06fd81fde6 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 @@ -10,11 +10,9 @@ interface SettingsRepository { suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) - @Throws fun saveLogMessage(message: String) - @Throws - suspend fun deleteDeprecatedLogs(maxSize: Int) + fun deleteDeprecatedLogs(maxSize: Int) suspend fun isSendTapHelpPreviewEnabled(): Boolean From e8c930481f56f6e793fedc661b2b9bf570ddcb26 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 18:13:49 +0500 Subject: [PATCH 86/87] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 22 ++++--- core/res/src/main/res/values-fr/strings.xml | 57 ++++++++++++------- core/res/src/main/res/values-ja/strings.xml | 12 +++- core/res/src/main/res/values-ru/strings.xml | 41 ++++++++++++- core/res/src/main/res/values/strings.xml | 9 ++- .../SetApprovalInProgressTransformer.kt | 5 +- 6 files changed, 106 insertions(+), 40 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index e0da81b392..1e9ed93914 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,7 +1,7 @@ Netzwerk wählen - Token hinzuf. + Token anlegen Token verwalten Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen. So scannt man @@ -88,6 +88,7 @@ Kopieren Adresse kopieren Erstellen + %1$s (%2$s) Eigen %d tag @@ -279,7 +280,7 @@ Rückmeldung Feedback zu Tangem Eine Transaktion kann nicht gesendet werden - diese Transaktion + Nur diese Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. Gib das Genehmigungslimit für das ausgewählte Token an Betrag %s @@ -447,7 +448,7 @@ Möchtest du den Aktivierungsprozess abbrechen? Erste Schritte Für die Karte, die du hinzufügen möchtest, wurde bereits eine andere Wallets erstellt. Wenn du Guthaben auf dieser Wallets hast, hebe es bitte ab, setze diese Karte zurück und füge sie als Backup hinzu. - Backups erstellen + Backups anlegen Lese mehr über die Seed-Phrase leer @@ -500,7 +501,7 @@ Gruppe erstellen Nach Guthaben Token organisieren - Grup. entf. + Gruppe löschen Wählen aus der Galerie aus Einstellungen Du hast keinen Zugriff auf deine Kamera gewährt @@ -639,7 +640,6 @@ Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert %1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. Ungültige Adresse - %1$s (%2$s) Transaktion gesendet Bereite das Scannen der Karte vor, die du einrichten möchtest. Entferne diese Wallet @@ -648,7 +648,7 @@ Aktiv Um deine Kryptos zu unstaken, klick hier. Die Anzahl der zu stakenden Krypros muss mindesten %s betragen - nicht gestakte beanspruche + Nicht gestakte beanspruche Jährliche prozentuale Rendite Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst. Effektiver Jahreszins @@ -672,6 +672,10 @@ Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking. Migrieren Natives Staking + Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden tag. + Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Stunde. + Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden Monat. + Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche. Verdiene Staking-Belohnungen Die Belohnungen werden sofort nach dem unstaken gestoppt. Der unstakingprozess dauert %s. Erneut binden @@ -679,8 +683,8 @@ Belohnungen erneut staken Widerrufen Neuwahl - automatisch - händisch + Automatisch + Händisch Block Tag Täglich @@ -694,7 +698,7 @@ Mehr staken Stake %s Staking beenden%s - gelocktes unlocken + Gelocktes unlocken Unstaken Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen Staking beenden diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 5e041ee2f3..d39ab86d92 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -6,10 +6,10 @@ Envoyez uniquement %1$s (%2$s) depuis les réseaux %3$s à cette adresse. L\'utilisation d\'autres jetons et réseaux peut entraîner une perte de fonds. Comment scanner Demander de l\'aide - Réessayer + Réessayez Cette fonctionnalité est désactivée en mode démo Raison : %s - Impossible d\'envoyer une transaction + Impossible d\'effectuer une transaction Le sélectionné ne prend pas en charge le réseau %1$s Pour activer le cryptage de la blockchain %1$s, vous devrez réinitialiser le portefeuille aux paramètres d\'usine. Veuillez retirer vos fonds avant de le faire pour vous assurer de ne pas les perdre, puis terminez le processus de réinitialisation. L\'accès au portefeuille actuel ne sera pas possible après la réinitialisation. Les jetons du réseau %1$s ne sont pas pris en charge par cette carte en raison d\'une limitation du micrologiciel. @@ -17,7 +17,7 @@ Cette carte n\'est pas conçue pour fonctionner avec Tangem Frais par défaut Activez les frais par défaut pour définir automatiquement les frais de transaction et ignorer la page Frais lors de l\'envoi de fonds. Vous pouvez toujours revenir sur cette page si nécessaire. - Allez dans les paramètres pour activer l\'authentification biométrique dans le Tangem App + Accédez aux paramètres pour activer l\'authentification biométrique dans l\'application Tangem Activer l\'authentification biométrique Cela supprimera tous les codes d\'accès des portefeuilles enregistrés. Toute opération ultérieure avec le portefeuille nécessitera la soumission du code d\'accès. La suppression de la carte enregistrée supprime de l\'application tous les portefeuilles enregistrés et leurs codes d\'accès. @@ -35,7 +35,7 @@ Compris Les soldes sont masqués Veuillez scanner la carte - Veuillez réessayer dans 30 secondes ou scanner la carte + Veuillez réessayer dans 30 secondes ou scannez la carte Trop de tentatives Vous avez désactivé l\'authentification biométrique sur votre téléphone et ne pourrez pas enregistrer de portefeuilles dans l\'application. Pour enregistrer des portefeuilles, veuillez activer la fonction d\'authentification biométrique dans les paramètres de votre téléphone. Démarrer le processus de sauvegarde @@ -86,6 +86,7 @@ Copier Copier l\'adresse Créer + %1$s (%2$s) Personnalisé %d jour @@ -97,7 +98,7 @@ Activer Activé Erreur - Explorer + Explorez Explorez l\'historique des transactions Explorateur Commissions @@ -134,13 +135,13 @@ Rechercher Rechercher des jetons Seed phrase - Sélectionner une action + Sélectionnez une action Vendre Envoyer Le serveur n\'est pas disponible, veuillez réessayer plus tard Partager - Signer - Signer et envoyer + Signez + Signez et envoyez Enjeu Staking Démarrer @@ -237,7 +238,7 @@ En attente du dépôt En attente du dépôt Remboursé - Je vous envoie + Transfert à votre compte En cours d\'envoi Envoyé Données fournies par le fournisseur. Le montant estimé est sujet à modification en raison des conditions du marché. @@ -269,8 +270,8 @@ Impossible de scanner une carte Commentaires Commentaires sur Tangem - Impossible d\'envoyer une transaction - Transaction + Impossible d\'effectuer une transaction + Cette transaction Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange. Spécifiez la limite approuvée pour le jeton sélectionné Montant %s @@ -284,7 +285,7 @@ Pour créer le portefeuille, appuyez sur la carte comme indiqué ci-dessus et ne la retirez pas jusqu\'à la fin de l\'opération Appuyez sur la carte n°%s du portefeuille Posez pour scanner - Touchez pour signer + Tapez pour signer Posez la carte Vous avez mis à jour vos données biométriques, scannez votre carte pour entrer Votre solde doit être supérieur à la valeur des frais pour effectuer un transfert @@ -327,11 +328,20 @@ Pour commencer à acheter, échanger ou recevoir cet actif, ajoutez ce jeton à au moins 1 réseau Cet actif n\'est pas disponible Ajouter au portfolio + Ajoutez un jeton + Réseaux disponibles Mon portfolio Marché Pour générer des adresses pour les réseaux sélectionnés, vous devez scanner votre carte Tangem + Impossible de charger les données… + Résultat + Voir les jetons de moins de 100 000 $ de capitalisation boursière + Afficher les jetons + Aucun résultat + Sélectionnez un réseau Sélectionnez un portefeuille Trier par + À propos de %s Idées Liens Métriques @@ -363,7 +373,7 @@ Finaliser la sauvegarde Recevoir des crypto-monnaies Scannez la carte principale - Ignorer pour plus tard + Finir plus tard Comment ça marche ? Générons toutes les clés sur votre carte et créons un portefeuille sécurisé Créer un portefeuille @@ -375,7 +385,7 @@ Succès ! Dans ce cas, vous devrez recommencer depuis le début. Voulez-vous quitter le processus d\'activation ? - Initialiser + Initialisation en cours Un autre portefeuille a déjà été créé sur la carte que vous essayez d\'ajouter. Si vous avez des fonds dans ce portefeuille, veuillez les retirer, puis réinitialiser cette carte et l\'ajouter comme sauvegarde. Sauvegarde en cours En savoir plus sur les seed phrases @@ -489,7 +499,7 @@ Vous avez spécifié une commission inférieure au montant recommandé, ce qui pourrait entraîner un retard dans votre transaction. Continuer? Raison : %1$s\nCode : %2$s La transaction est incomplète - Somme + Montant Vous pouvez définir vos frais de transaction en ajustant la valeur dans le champ Satoshi par vByte. Les frais qui seront facturés pour votre transaction. Vous pouvez définir votre propre valeur. Frais maximum @@ -500,7 +510,7 @@ KAS pour UTXO %1$s, %2$s Adresse - Destination Tag + ID de destination Entrez l\'adresse L\'adresse est la même que celle de votre portefeuille Tag invalide. Il ne sera pas ajouté à la transaction. @@ -558,7 +568,7 @@ Appuyez sur n\'importe quel champ pour le modifier Envoyer %s Vous envoyez **%1$s** incluant des frais de réseau de %2$s - Vous envoyez **%1$s** and %2$s + Vous envoyez **%1$s** et %2$s Envoi de %s Total Sera envoyé %1$s et %2$s @@ -567,9 +577,8 @@ 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 %1$s est un actif du réseau Tron. Pour calculer les frais et effectuer une transaction, déposez du Tron (TRX) sur votre compte. Adresse incorrecte - %1$s (%2$s) Transaction envoyée - Préparez-vous à numériser la carte que vous souhaitez configurer. + Scannez la carte que vous souhaitez configurer. Oublier le portefeuille Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau. Nom @@ -597,6 +606,7 @@ Native staking Gagnez des récompenses de staking Récompenses + Stake verrouillé Staker plus Non-staké Vérifiez non-stakés pour réclamer vos actifs @@ -611,12 +621,13 @@ Le portefeuille pour tous Découvrez Tangem Échangez, achetez des NFT, faites des prêts et des dépôts dans plus de 100 services décentralisés différents - Compatible Web 3.0 + Compatible avec Web 3.0 Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %s depuis l\'échange vers l\'adresse de l\'utilisateur. Le montant comprend les frais du fournisseur de services. Frais Tous les échanges décentralisés nécessitent des approbations pour empêcher les smart contracts d\'accéder à votre portefeuille sans votre permission. Par conception, les smart contracts ne peuvent pas accéder à vos jetons sans votre approbation. En « déverrouillant » vos jetons, vous autorisez le smart contract 1-inch à les dépenser. Les mineurs du réseau reçoivent des frais de gaz (payés par vous) pour enregistrer cette action sur la blockchain. Vous pouvez échanger votre jeton après avoir donné votre approbation. Approuver + Erreur d\'estimation des frais. Veuillez envoyer vos commentaires à l\'équide de support. Vous échangez Échanger ce montant de jetons sélectionnés aura un impact significatif sur les prix et réduira votre résultat. Fonds insuffisants @@ -660,6 +671,7 @@ Opération de : %s à : %s + Réessayez Vous avez scanné la même carte. Pour créer un portefeuille jumeau, vous devez scanner la carte portant le numéro %d Vous avez scanné une mauvaise carte jumelle. S\'il vous plaît, essayez-en un autre Celle que vous tenez dans vos mains et l\'autre portant le numéro %s\n\nLes deux cartes peuvent être utilisées pour extraire des fonds de ce portefeuille. @@ -692,7 +704,7 @@ Détails de la transaction :\nDe : %1$s\nÀ : %2$s\nMontant : %3$s Le presse-papiers contient un code WalletConnect. Utilisez la valeur copiée ou scannez le QR-code Demande de création d\'une transaction pour %1$s\n%2$s\n\nMontant : %3$s\nFrais : %4$s\nTotal : %5$s\nSolde : %6$s - Impossible d\'envoyer la transaction. Fonds insuffisants. + Impossible d\'effectuer la transaction. Fonds insuffisants. Échec de l\'établissement de la session WalletConnect. Veuillez réessayer plus tard. Échec de la signature du message. Veuillez réessayer Échec de l\'établissement de la session WalletConnect : erreur de délai d\'attente. Veuillez réessayer plus tard. @@ -731,7 +743,7 @@ BNB Beacon Chain va s\'arrêter de fonctionner Pas terrible J\'aime - Ok, je l\'ai! + Ok, compris! Vraiment cool ! Rafraîchir Vous êtes actuellement en mode démo @@ -750,6 +762,7 @@ Pour effectuer une transaction, vous devez déposer %1$s %2$s Impossible de couvrir %s frais Le montant à recevoir doit être d\'au moins %s + Cela peut se produire car le fournisseur n\'est actuellement pas en mesure d\'échanger la paire que vous avez sélectionnée. Veuillez patienter un instant et réessayer. (Code %@) Service temporairement indisponible Le nombre de jetons à échanger ne doit pas dépasser %s Le montant à échanger doit être d\'au moins %s diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 8ebb799edb..e5c43474a1 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -87,6 +87,7 @@ コピー アドレスをコピー 作成 + %1$s ( %2$s ) 設定 %d 日 @@ -124,6 +125,7 @@ パスフレーズ ペースト %1$s-%2$s + %1$s — %2$s 続きを読む 受け取る 拒否 @@ -286,7 +288,7 @@ 許可を与える 無制限 カードを注文 - カードをスキャン + スキャン アクセスコードを変更するには、上図のようにカードをタップし、操作が終了するまで取り外さないでください。 パスコードを変更するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。 ウォレットを作成するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。 @@ -626,7 +628,6 @@ 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 無効なアドレス - %1$s ( %2$s ) 取引が送信されました セットアップしたいカードをスキャンするために準備してください。 ウォレット削除 @@ -636,6 +637,7 @@ 資産のステーキングを解除するには、ここをクリックしてください。 ステーキング金額は %s 以上である必要があります ステーキング解除分を請求する + 年率 ステーキングに参加することで得られる年間収益率。 APR 利用可能 @@ -658,6 +660,10 @@ ステーキングへの参加を有効にするために割り当てられた時間。 移行 ネイティブステーキング + ステーキングにより%1$sを獲得できます。ステーキング報酬は毎日受け取れます。 + ステーキングにより%1$sを獲得できます。ステーキング報酬は1時間ごとに受け取れます。 + ステーキングにより%1$sを獲得できます。ステーキング報酬は毎月受け取れます。 + ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。 ステーキング報酬を獲得 ステーキング解除後、報酬の獲得はすぐに停止します。ステーキング解除プロセスには%sかかります。 再結束 @@ -842,6 +848,8 @@ 取引を行うには、 %1$s %2$sを入金する必要があります。 %s 手数料を支払えません 受け取る金額は、 %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 9df99375b9..e89bb61d95 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -69,6 +69,7 @@ Недостаточно ADA Принять Доступ запрещен + Все Разрешить Применить Одобрение @@ -88,6 +89,7 @@ Копировать Скопировать адрес Создать + %1$s (%2$s) Свое %d день @@ -101,6 +103,7 @@ Включить Включено Ошибка + Обменять Обозреватель Посмотреть историю транзакций Обозреватель @@ -136,7 +139,7 @@ Переименовать Сохранить Сохранить изменения - Поиск на маркете + Поиск Поиск токенов Seed-фраза Выберите действие @@ -340,20 +343,24 @@ Чтобы купить, обменять или получить данный токен вам нужно добавить его к себе в портфель Этот актив недоступен Добавить в портфель + Добавить Доступные сети Мой портфель Рынок Чтобы создать адреса для выбранных сетей, отсканируйте вашу карту Tangem кошелька + Быстрые действия Результат Токены с капитализацией меньше 100к Показать токены Нет результата + Выберите сеть Выберите кошелек 1мин 1год 24ч + Все Опытные покупатели Рейтинг @@ -361,8 +368,22 @@ Лидеры роста Лидеры падения В тренде + О %s + + На основе %d оценки + На основе %d оценок + На основе %d оценок + На основе %d оценок + Сайт блокчейна + Покупательское предпочтение Разница между объемом покупателей и продавцов + Циркулирующее предложение + Общее количество монет, доступных для торговли и находящихся в обращении на рынке + Опытные покупкатели + Полностью разбавленная капитализация + Общая теоретическая стоимость криптовалюты, если все монеты, которые могут существовать, находятся в обращении, включая те, которые в настоящее время не обращаются + Дата создания Высокий Держатели Изменение количества держателей токенов в течение выбранного периода времени @@ -370,15 +391,20 @@ Ссылки Ликвидность Изменение объема ликвидности, доступной для токена в течение указанного периода времени. + Индекс ликвидности Низкий Рыночная капитализация Общая рыночная стоимость криптовалюты, рассчитываемая путем умножения текущей цены монеты на общее количество монет в обращении. Рыночный рейтинг + Максимальный объем Метрики Официальные ссылки + Динамика цены Репозиторий Оценка безопасности Социальные + Общее предложение + Максимальное количество монет или токенов, которое может когда-либо существовать для выбранной криптовалюты. Объем торгов (24ч) Общая сумма криптовалюты, которая была продана за последние 24 часа, показывающая уровень активности и ликвидности на рынке. Вам необходимо установить единый код доступа для защиты всех ваших карт @@ -431,7 +457,7 @@ Ваша seed-фраза - + %d слово %d слова %d слов %d слов @@ -629,10 +655,12 @@ Для завершения стейкинга нажмите сюда Сумма для стейкинга должна быть не менее %s Забрать средства + Годовая процентная ставка Годовой процентный доход, который вы можете получить от участия в стейкинге. APR Доступно Средння ставка вознаграждения + Что такое Стейкинг? %s оценка доходности Позиция в рынке Метрики @@ -671,6 +699,7 @@ Месяц Неделя Вознаграждения + Стейкинг закрыт Застейкать еще Застейкать %s Разблокировать @@ -678,7 +707,9 @@ Проверьте процесс завершения стейкинга, чтобы вывести свои средства. Завершение стейкинга Валидатор + Валидаторы Проголосовать + Голосование заблокировано Вывод Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек @@ -698,6 +729,7 @@ Комиссии Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. Подтвердить + Ошибка расчета комиссии. Пожалуйста, отправьте информацию в поддержку. Вы отправляете Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств @@ -741,6 +773,7 @@ Операция от: %s на: %s + Попробовать снова Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька. @@ -807,6 +840,8 @@ Настройки кошелька Tangem Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку + Процесс выдачи разрешения уже в работе и скоро будет завершен + Выдача разрешения Похоже, что процесс активации карт не был завершен корректно. Это могло быть вызвано проблемой взаимодействия с модулем NFC либо некорректным прикладыванием карты к телефону. Пожалуйста, обратитесь в нашу службу поддержки для уточнения деталей. Ошибка активации По решению разработчиков сети BNB стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении или сторонние сервисы, чтобы перевести средства в cеть BNB Smart Chain. @@ -832,6 +867,8 @@ Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s Невозможно покрыть комиссию %s Сумма получения не может быть менее %s + Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %@) + Выбранная пара временно недоступна Cервис временно недоступен Сумма для обмена должна быть не более %s Сумма для обмена должна быть не менее %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 956b064d05..79135b1d0d 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -100,6 +100,7 @@ Enable Enabled Error + Exchange Explore Explore transaction history Explorer @@ -135,7 +136,7 @@ Rename Save Save changes - Search through the market + Search Search tokens Seed phrase Select action @@ -339,7 +340,7 @@ To buy, exchange, or receive this asset, add it to your portfolio This asset is not available Add to portfolio - Add token + Add Available networks My portfolio Market @@ -832,6 +833,8 @@ Wallet settings Tangem Use %s or scan a card to unlock access to your wallet + The permission-granting process is currently underway and will be completed shortly + Approval in Progress It seems that the card activation was not completed correctly. This could be due to an issue with your device\'s NFC module or incorrect tapping of the card to your device. Please contact our Support team for assistance. Activation error According to BNB network developers, support for the BEP-2 standard will end in June 2024. To avoid losing assets with this standard, please convert them to the BEP-20 standard. Use our swap service or third-party services to transfer funds to the BNB Smart Chain network. @@ -857,6 +860,8 @@ 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 + This may occur because the provider is currently unable to exchange your selected pair. Please wait a moment and try again. (Code %@) + Selected pair temporarily unavailable 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/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalInProgressTransformer.kt index f238a8c764..f9a5418f89 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalInProgressTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalInProgressTransformer.kt @@ -13,11 +13,10 @@ internal object SetApprovalInProgressTransformer : Transformer { val state = prevState.confirmationState as? StakingStates.ConfirmationState.Data val notifications = state?.notifications?.toMutableList() ?: mutableListOf() - // todo staking [REDACTED_TASK_KEY] replace with correct text notifications.add( StakingNotification.Warning.TransactionInProgress( - title = resourceReference(R.string.warning_express_approval_in_progress_title), - description = resourceReference(R.string.warning_express_approval_in_progress_message), + title = resourceReference(R.string.warning_approval_in_progress_title), + description = resourceReference(R.string.warning_approval_in_progress_message), ), ) From d1370f4ea5496544c7108ce56ee36e71547fff49 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Aug 2024 17:59:34 +0300 Subject: [PATCH 87/87] 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 74b8993062..6a52565547 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,9 +88,9 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-736" +tangemBlockchainSdk = "release-app_5.14-742" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-378" +tangemCardSdk = "release-app_5.14-379" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem16" #tangemVico = "0.0.1" # Keep it! - used for local builds ^