From 7cc1bb7379acb96095001480cc571839adba371b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jul 2024 18:55:40 +0500 Subject: [PATCH 01/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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) {