Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-12 14:38:46 +03:00
parent 54800d32cc
commit faa58cf142
39 changed files with 722 additions and 245 deletions

View file

@ -3,6 +3,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
import com.tangem.domain.markets.GetTokenPriceChartUseCase
import com.tangem.domain.markets.GetTokenQuotesUseCase
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import dagger.Module
import dagger.Provides
@ -33,4 +34,10 @@ object MarketsDomainModule {
fun provideGetTokenMarketInfoUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenMarketInfoUseCase {
return GetTokenMarketInfoUseCase(marketsTokenRepository = marketsTokenRepository)
}
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenQuotesUseCase {
return GetTokenQuotesUseCase(marketsTokenRepository = marketsTokenRepository)
}
}

View file

@ -295,7 +295,7 @@ private fun MarketChartPreview(
updateLook {
it.copy(
xAxisFormatter = { value ->
value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd)
value.toLong().toTimeFormat(DateTimeFormatters.dateMMMdd)
},
yAxisFormatter = { value ->
value.setScale(3, RoundingMode.HALF_UP).toPlainString()

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="add_custom_token_choose_network">Netzwerk wählen</string>
<string name="add_custom_token_title">Benutzerdef. Token hinzuf.</string>
<string name="add_custom_token_title">Token anlegen</string>
<string name="add_tokens_title">Token verwalten</string>
<string name="address_qr_code_message_format">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.</string>
<string name="alert_button_how_to_scan">So scannt man</string>
@ -88,7 +88,7 @@
<string name="common_copy">Kopieren</string>
<string name="common_copy_address">Adresse kopieren</string>
<string name="common_create">Erstellen</string>
<string name="common_custom">Benutzerdef.</string>
<string name="common_custom">Eigen</string>
<plurals name="common_days">
<item quantity="one">%d tag</item>
<item quantity="other">%d tage</item>
@ -126,6 +126,7 @@
<string name="common_passphrase">Passphrase</string>
<string name="common_paste">Einfügen</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
<string name="common_read_more">Weiterlesen</string>
<string name="common_receive">Empfangen</string>
<string name="common_reject">Ablehnen</string>
@ -278,7 +279,7 @@
<string name="feedback_subject_support">Rückmeldung</string>
<string name="feedback_subject_support_tangem">Feedback zu Tangem</string>
<string name="feedback_subject_tx_failed">Eine Transaktion kann nicht gesendet werden</string>
<string name="give_permission_current_transaction">Aktuelle Transaktion</string>
<string name="give_permission_current_transaction">Nur diese</string>
<string name="give_permission_fee_footer">Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen.</string>
<string name="give_permission_policy_type_footer">Gib das Genehmigungslimit für das ausgewählte Token an</string>
<string name="give_permission_rows_amount">Betrag %s</string>
@ -341,7 +342,7 @@
<string name="markets_available_networks">Verfügbare Netzwerke</string>
<string name="markets_common_my_portfolio">Mein Portfolio</string>
<string name="markets_common_title">Markt</string>
<string name="markets_generate_addresses_notification">Um Adressen für ausgewählte Netzwerke zu generieren, musst du eine Tangem-Karte scannen/ einsetzen</string>
<string name="markets_generate_addresses_notification">Um Adressen für ausgewählte Netzwerke zu generieren, musst du eine Tangem-Karte scannen</string>
<string name="markets_loading_error_title">Die Daten konnten nicht geladen werden...</string>
<string name="markets_quick_actions">Schnelle Aktionen</string>
<string name="markets_search_result_title">Ergebnis</string>
@ -446,7 +447,7 @@
<string name="onboarding_exit_alert_title">Möchtest du den Aktivierungsprozess abbrechen?</string>
<string name="onboarding_getting_started">Erste Schritte</string>
<string name="onboarding_linking_error_card_with_wallets">Für die Karte, die du hinzufügen möchtest, wurde bereits eine andere Wallets erstellt. Wenn du Guthaben auf dieser Wallets hast, hebe es bitte ab, setze diese Karte zurück und füge sie als Backup hinzu.</string>
<string name="onboarding_navbar_title_creating_backup">Erstellen eines Backups</string>
<string name="onboarding_navbar_title_creating_backup">Backups anlegen</string>
<string name="onboarding_seed_button_read_more">Lese mehr über die Seed-Phrase</string>
<plurals name="onboarding_seed_generate_message_words_count">
<item quantity="one">leer</item>
@ -499,7 +500,7 @@
<string name="organize_tokens_group">Gruppe erstellen</string>
<string name="organize_tokens_sort_by_balance">Nach Guthaben</string>
<string name="organize_tokens_title">Token organisieren</string>
<string name="organize_tokens_ungroup">Gruppierung aufh.</string>
<string name="organize_tokens_ungroup">Gruppe löschen</string>
<string name="qr_scanner_camera_denied_gallery_button">Wählen aus der Galerie aus</string>
<string name="qr_scanner_camera_denied_settings_button">Einstellungen</string>
<string name="qr_scanner_camera_denied_text">Du hast keinen Zugriff auf deine Kamera gewährt</string>
@ -647,7 +648,7 @@
<string name="staking_active">Aktiv</string>
<string name="staking_active_footer">Um deine Kryptos zu unstaken, klick hier.</string>
<string name="staking_amount_requirement_error">Die Anzahl der zu stakenden Krypros muss mindesten %s betragen</string>
<string name="staking_claim_unstaked">nicht gestakte beanspruche</string>
<string name="staking_claim_unstaked">Nicht gestakte beanspruche</string>
<string name="staking_details_annual_percentage_rate">Jährliche prozentuale Rendite</string>
<string name="staking_details_annual_percentage_rate_info">Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst.</string>
<string name="staking_details_apr">Effektiver Jahreszins</string>
@ -671,7 +672,10 @@
<string name="staking_details_warmup_period_info">Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking.</string>
<string name="staking_migrate">Migrieren</string>
<string name="staking_native">Natives Staking</string>
<string name="staking_notification_earn_rewards_text">Mit Staking kannst du %1$s verdienen. Deine Staking-Belohnungen kommen alle ~%2$s Tage.</string>
<string name="staking_notification_earn_rewards_text_period_day">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden tag.</string>
<string name="staking_notification_earn_rewards_text_period_hour">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Stunde.</string>
<string name="staking_notification_earn_rewards_text_period_month">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden Monat.</string>
<string name="staking_notification_earn_rewards_text_period_week">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche.</string>
<string name="staking_notification_earn_rewards_title">Verdiene Staking-Belohnungen</string>
<string name="staking_notification_unstake_text">Die Belohnungen werden sofort nach dem unstaken gestoppt. Der unstakingprozess dauert %s.</string>
<string name="staking_rebond">Erneut binden</string>
@ -679,10 +683,11 @@
<string name="staking_restake_rewards">Belohnungen erneut staken</string>
<string name="staking_revoke">Widerrufen</string>
<string name="staking_revote">Neuwahl</string>
<string name="staking_reward_claiming_auto">automatisch</string>
<string name="staking_reward_claiming_manual">händisch</string>
<string name="staking_reward_claiming_auto">Automatisch</string>
<string name="staking_reward_claiming_manual">Händisch</string>
<string name="staking_reward_schedule_block">Block</string>
<string name="staking_reward_schedule_day">Tag</string>
<string name="staking_reward_schedule_each_day">Täglich</string>
<string name="staking_reward_schedule_epoch">Epoche</string>
<string name="staking_reward_schedule_era">Ära</string>
<string name="staking_reward_schedule_hour">Stunde</string>
@ -693,7 +698,7 @@
<string name="staking_stake_more">Mehr staken</string>
<string name="staking_title_stake">Stake %s</string>
<string name="staking_title_unstake">Staking beenden%s</string>
<string name="staking_unlocked_locked">gelocktes unlocken</string>
<string name="staking_unlocked_locked">Gelocktes unlocken</string>
<string name="staking_unstaked">Unstaken</string>
<string name="staking_unstaked_footer">Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen</string>
<string name="staking_unstaking">Staking beenden</string>
@ -834,7 +839,7 @@
<string name="warning_backup_errors_title">Aktivierungsfehler</string>
<string name="warning_beacon_chain_retirement_content">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.</string>
<string name="warning_beacon_chain_retirement_title">BNB Beacon Chain wird abgeschaltet</string>
<string name="warning_button_could_be_better">ausbaufähig</string>
<string name="warning_button_could_be_better">Ausbaufähig</string>
<string name="warning_button_like_it">Gefällt mir</string>
<string name="warning_button_ok">OK, habe ich verstanden!</string>
<string name="warning_button_really_cool">Echt toll!</string>

View file

@ -123,6 +123,7 @@
<string name="common_passphrase">Passphrase</string>
<string name="common_paste">Coller</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
<string name="common_read_more">En savoir plus</string>
<string name="common_receive">Recevoir</string>
<string name="common_reject">Rejeter</string>
@ -269,7 +270,7 @@
<string name="feedback_subject_support">Commentaires</string>
<string name="feedback_subject_support_tangem">Commentaires sur Tangem</string>
<string name="feedback_subject_tx_failed">Impossible d\'envoyer une transaction</string>
<string name="give_permission_current_transaction">Transaction en cours</string>
<string name="give_permission_current_transaction">Transaction</string>
<string name="give_permission_fee_footer">Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange.</string>
<string name="give_permission_policy_type_footer">Spécifiez la limite approuvée pour le jeton sélectionné</string>
<string name="give_permission_rows_amount">Montant %s</string>
@ -328,7 +329,7 @@
<string name="markets_add_to_portfolio_button">Ajouter au portfolio</string>
<string name="markets_common_my_portfolio">Mon portfolio</string>
<string name="markets_common_title">Marché</string>
<string name="markets_generate_addresses_notification">Pour générer des adresses pour les réseaux sélectionnés, vous devez scanner votre carte Tangem.</string>
<string name="markets_generate_addresses_notification">Pour générer des adresses pour les réseaux sélectionnés, vous devez scanner votre carte Tangem</string>
<string name="markets_select_wallet">Sélectionnez un portefeuille</string>
<string name="markets_sort_by_title">Trier par</string>
<string name="markets_token_details_insights">Idées</string>
@ -594,7 +595,6 @@
<string name="staking_details_warmup_period">Période d\'échauffement</string>
<string name="staking_details_warmup_period_info">Le temps imparti pour activer la participation au staking.</string>
<string name="staking_native">Native staking</string>
<string name="staking_notification_earn_rewards_text">Le staking vous permet d\'en gagner %1$s. Vos récompenses de staking arrivent tous les ~%2$s jours.</string>
<string name="staking_notification_earn_rewards_title">Gagnez des récompenses de staking</string>
<string name="staking_rewards">Récompenses</string>
<string name="staking_stake_more">Staker plus</string>

View file

@ -14,7 +14,6 @@
<string name="common_save_changes">Mantieni le modifiche</string>
<string name="common_send">Invia</string>
<string name="common_success">Con successo</string>
<string name="common_warning">Avviso</string>
<string name="details_manage_security_access_code">Codice di accesso</string>
<string name="details_manage_security_access_code_description">Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto</string>
<string name="details_manage_security_long_tap">Mantenimento della carta</string>

View file

@ -124,6 +124,7 @@
<string name="common_passphrase">パスフレーズ</string>
<string name="common_paste">ペースト</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
<string name="common_read_more">続きを読む</string>
<string name="common_receive">受け取る</string>
<string name="common_reject">拒否</string>
@ -276,7 +277,7 @@
<string name="feedback_subject_support">フィードバック</string>
<string name="feedback_subject_support_tangem">Tangemへのフィードバック</string>
<string name="feedback_subject_tx_failed">取引を送信できません</string>
<string name="give_permission_current_transaction">現在の取引</string>
<string name="give_permission_current_transaction">取引</string>
<string name="give_permission_fee_footer">ネットワークは、あなたがトークンのスワップを承認していることを確認するために、トークン承認手数料を請求します。</string>
<string name="give_permission_policy_type_footer">選択したトークンの承認制限を指定します</string>
<string name="give_permission_rows_amount">金額%s</string>
@ -636,7 +637,7 @@
<string name="staking_active_footer">資産のステーキングを解除するには、ここをクリックしてください。</string>
<string name="staking_amount_requirement_error">ステーキング金額は %s 以上である必要があります</string>
<string name="staking_claim_unstaked">ステーキング解除分を請求する</string>
<string name="staking_details_annual_percentage_rate">APY</string>
<string name="staking_details_annual_percentage_rate">年率</string>
<string name="staking_details_annual_percentage_rate_info">ステーキングに参加することで得られる年間収益率。</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_available">利用可能</string>
@ -659,7 +660,10 @@
<string name="staking_details_warmup_period_info">ステーキングへの参加を有効にするために割り当てられた時間。</string>
<string name="staking_migrate">移行</string>
<string name="staking_native">ネイティブステーキング</string>
<string name="staking_notification_earn_rewards_text">ステーキングにより%1$sを獲得できます。ステーキング報酬は ~ %2$s日ごとに届きます。</string>
<string name="staking_notification_earn_rewards_text_period_day">ステーキングにより%1$sを獲得できます。ステーキング報酬は毎日受け取れます。</string>
<string name="staking_notification_earn_rewards_text_period_hour">ステーキングにより%1$sを獲得できます。ステーキング報酬は1時間ごとに受け取れます。</string>
<string name="staking_notification_earn_rewards_text_period_month">ステーキングにより%1$sを獲得できます。ステーキング報酬は毎月受け取れます。</string>
<string name="staking_notification_earn_rewards_text_period_week">ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。</string>
<string name="staking_notification_earn_rewards_title">ステーキング報酬を獲得</string>
<string name="staking_notification_unstake_text">ステーキング解除後、報酬の獲得はすぐに停止します。ステーキング解除プロセスには%sかかります。</string>
<string name="staking_rebond">再結束</string>
@ -671,6 +675,7 @@
<string name="staking_reward_claiming_manual">手動</string>
<string name="staking_reward_schedule_block">ブロック</string>
<string name="staking_reward_schedule_day"></string>
<string name="staking_reward_schedule_each_day">毎日</string>
<string name="staking_reward_schedule_epoch">エポック</string>
<string name="staking_reward_schedule_era">時代</string>
<string name="staking_reward_schedule_hour"></string>
@ -686,6 +691,7 @@
<string name="staking_unstaked_footer">資産を請求するために、unstakedを確認してください</string>
<string name="staking_unstaking">ステーキング解除</string>
<string name="staking_validator">バリデーター</string>
<string name="staking_validators">バリデーター</string>
<string name="staking_vote">投票する</string>
<string name="staking_vote_locked">投票はロックされています</string>
<string name="staking_withdraw">引き出す</string>

View file

@ -47,7 +47,7 @@
</plurals>
<string name="card_settings_access_code_recovery_disabled_description">Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте.</string>
<string name="card_settings_access_code_recovery_enabled_description">Использовать эту карту для сброса кода доступа на других картах в этом кошельке</string>
<string name="card_settings_access_code_recovery_title">Восстановление кода доступа</string>
<string name="card_settings_access_code_recovery_title">Восстановление доступа</string>
<string name="card_settings_action_sheet_reset">Сбросить</string>
<string name="card_settings_action_sheet_title">Вы уверены, что хотите это сделать?</string>
<string name="card_settings_change_access_code">Смена кода доступа</string>
@ -128,6 +128,7 @@
<string name="common_passphrase">Парольная фраза</string>
<string name="common_paste">Вставить</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
<string name="common_read_more">Подробнее</string>
<string name="common_receive">Получить</string>
<string name="common_reject">Отклонить</string>
@ -628,7 +629,6 @@
<string name="staking_active_footer">Для завершения стейкинга нажмите сюда</string>
<string name="staking_amount_requirement_error">Сумма для стейкинга должна быть не менее %s</string>
<string name="staking_claim_unstaked">Забрать средства</string>
<string name="staking_details_annual_percentage_rate">APY</string>
<string name="staking_details_annual_percentage_rate_info">Годовой процентный доход, который вы можете получить от участия в стейкинге.</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_available">Доступно</string>
@ -650,13 +650,26 @@
<string name="staking_details_warmup_period_info">Время, необходимое для начала процесса стейкинга и активации процесса начисления наград</string>
<string name="staking_migrate">Переместить</string>
<string name="staking_native">Нативный стейкинг</string>
<string name="staking_notification_earn_rewards_text">Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый %2$s</string>
<string name="staking_notification_earn_rewards_text_period_day">Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый день.</string>
<string name="staking_notification_earn_rewards_text_period_hour">Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый час.</string>
<string name="staking_notification_earn_rewards_text_period_month">Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый месяц.</string>
<string name="staking_notification_earn_rewards_text_period_week">Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждую неделю.</string>
<string name="staking_notification_earn_rewards_title">Получите награду за стейкинг</string>
<string name="staking_notification_unstake_text">Награда перестанет начисляться сразу после завершения стейкинга. Процесс завершения длится %s.</string>
<string name="staking_restake">Повторный стейкинг</string>
<string name="staking_restake_rewards">Застейкать вознаграждения</string>
<string name="staking_revoke">Отозвать</string>
<string name="staking_revote">Переголосовать</string>
<string name="staking_reward_claiming_auto">Автоматически</string>
<string name="staking_reward_claiming_manual">Вручную</string>
<string name="staking_reward_schedule_block">Блок</string>
<string name="staking_reward_schedule_day">День</string>
<string name="staking_reward_schedule_each_day">Каждый день</string>
<string name="staking_reward_schedule_epoch">Эпоха</string>
<string name="staking_reward_schedule_era">Эра</string>
<string name="staking_reward_schedule_hour">Час</string>
<string name="staking_reward_schedule_month">Месяц</string>
<string name="staking_reward_schedule_week">Неделя</string>
<string name="staking_rewards">Вознаграждения</string>
<string name="staking_stake_more">Застейкать еще</string>
<string name="staking_title_stake">Застейкать %s</string>

View file

@ -130,6 +130,7 @@
<string name="common_passphrase">Парольна фраза</string>
<string name="common_paste">Вставити</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
<string name="common_read_more">Детальніше</string>
<string name="common_receive">Отримати</string>
<string name="common_reject">Відхилити</string>
@ -282,7 +283,7 @@
<string name="feedback_subject_support">Звернення в підтримку</string>
<string name="feedback_subject_support_tangem">Звернення в підтримку Tangem</string>
<string name="feedback_subject_tx_failed">Не вдається відправити транзакцію</string>
<string name="give_permission_current_transaction">Поточна транзакція</string>
<string name="give_permission_current_transaction">Транзакція</string>
<string name="give_permission_fee_footer">Мережа стягує комісію за схвалення токену за підтвердження, що саме ви дозволяєте використовувати ваш токен для обміну.</string>
<string name="give_permission_policy_type_footer">Вкажіть ліміт доступу для обраного токена</string>
<string name="give_permission_rows_amount">Кількість %s</string>
@ -347,7 +348,7 @@
<string name="markets_available_networks">Доступні мережі</string>
<string name="markets_common_my_portfolio">Моє портфоліо</string>
<string name="markets_common_title">Маркет</string>
<string name="markets_generate_addresses_notification">Щоб згенерувати адреси для обраних мереж, потрібно відсканувати свою картку Tangem.</string>
<string name="markets_generate_addresses_notification">Щоб згенерувати адреси для обраних мереж, потрібно відсканувати свою картку Tangem</string>
<string name="markets_loading_error_title">Не вдалося завантажити дані...</string>
<string name="markets_quick_actions">Швидкі дії</string>
<string name="markets_search_result_title">Результат</string>
@ -682,7 +683,10 @@
<string name="staking_details_warmup_period_info">Відведений час для активації участі в стейкінгу.</string>
<string name="staking_migrate">Перемістити</string>
<string name="staking_native">Нативний стейкінг</string>
<string name="staking_notification_earn_rewards_text">Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг надходять кожні ~%2$s днів.</string>
<string name="staking_notification_earn_rewards_text_period_day">Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щодня.</string>
<string name="staking_notification_earn_rewards_text_period_hour">Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються кожну годину.</string>
<string name="staking_notification_earn_rewards_text_period_month">Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щомісяця.</string>
<string name="staking_notification_earn_rewards_text_period_week">Стейкінг дозволяє заробляти %1$s. Ваші винагороди за стейкінг зараховуються щотиждня.</string>
<string name="staking_notification_earn_rewards_title">Отримуйте винагороду за стейкінг</string>
<string name="staking_notification_unstake_text">Винагороди припиняють нараховуватися одразу після того, як ви знімаєте ставку. Процес зняття займає %s.</string>
<string name="staking_rebond">Зʼєднати ще раз</string>

View file

@ -9,9 +9,6 @@
<string name="alert_failed_to_send_transaction_message">原因:%s</string>
<string name="alert_failed_to_send_transaction_title">無法發送交易</string>
<string name="alert_manage_tokens_unsupported_message">此卡不支持%1$s網路上的代幣因為韌體限制</string>
<string name="alert_negative_app_rate_sent_message">感謝您的反饋。我們會盡快回复</string>
<string name="alert_negative_app_rate_sent_title">你的建議已送出</string>
<string name="alert_troubleshooting_scan_card_message">請嘗試完全按照動畫中顯示的方式點擊卡片或請求支持</string>
<string name="alert_troubleshooting_scan_card_title">有困難在掃描卡上嗎?</string>
<string name="alert_unsupported_card">此卡不適用於此app</string>
<string name="app_settings_enable_biometrics_description">轉到設置以在 Tangem App 中啟用生物識別身份驗證</string>
@ -56,7 +53,6 @@
<string name="common_create">創造</string>
<string name="common_delete">刪除</string>
<string name="common_disabled">禁用</string>
<string name="common_disconnect">斷開連接</string>
<string name="common_done">完成</string>
<string name="common_enable">允許</string>
<string name="common_enabled">啟用</string>
@ -69,7 +65,6 @@
<string name="common_origin_card">主卡片</string>
<string name="common_reject">拒絕</string>
<string name="common_rename">重新命名</string>
<string name="common_retry">重試</string>
<string name="common_save_changes">保存設置</string>
<string name="common_search">搜索</string>
<string name="common_search_tokens">搜尋代幣</string>
@ -88,7 +83,6 @@
<string name="common_transactions">交易</string>
<string name="common_understand">我了解</string>
<string name="common_unreachable">無法觸達</string>
<string name="common_warning">警告</string>
<string name="common_yes"></string>
<string name="contract_address_copied_message">已複製代幣地址</string>
<string name="currency_subtitle_expanded">支持的網路</string>
@ -121,7 +115,6 @@
<string name="details_row_title_currency">App Currency</string>
<string name="details_row_title_issuer">發行人</string>
<string name="details_row_title_signed_hashes">簽署</string>
<string name="details_security_management_warning">如果您忘記密碼,您將無法使用您的資金。無法恢復代碼</string>
<string name="details_title">更多</string>
<string name="disclaimer_error_loading">檢查您的網路連接或切換到其他網絡</string>
<string name="disclaimer_title">服務條款</string>

View file

@ -126,6 +126,7 @@
<string name="common_passphrase">Passphrase</string>
<string name="common_paste">Paste</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_range_with_space">%1$s — %2$s</string>
<string name="common_read_more">Read more</string>
<string name="common_receive">Receive</string>
<string name="common_reject">Reject</string>
@ -667,7 +668,10 @@
<string name="staking_details_warmup_period_info">The allocated time for activating participation in staking.</string>
<string name="staking_migrate">Migrate</string>
<string name="staking_native">Native staking</string>
<string name="staking_notification_earn_rewards_text">Staking allow you to earn %1$s. Your staking rewards arrive every ~%2$s days.</string>
<string name="staking_notification_earn_rewards_text_period_day">Staking allow you to earn %1$s. Your staking rewards arrive every day.</string>
<string name="staking_notification_earn_rewards_text_period_hour">Staking allow you to earn %1$s. Your staking rewards arrive every hour.</string>
<string name="staking_notification_earn_rewards_text_period_month">Staking allow you to earn %1$s. Your staking rewards arrive every month.</string>
<string name="staking_notification_earn_rewards_text_period_week">Staking allow you to earn %1$s. Your staking rewards arrive every week.</string>
<string name="staking_notification_earn_rewards_title">Earn staking rewards</string>
<string name="staking_notification_unstake_text">Rewards stop accruing immediately after you unstake. The unstaking process takes %s.</string>
<string name="staking_rebond">Rebond</string>
@ -679,6 +683,7 @@
<string name="staking_reward_claiming_manual">Manual</string>
<string name="staking_reward_schedule_block">Block</string>
<string name="staking_reward_schedule_day">Day</string>
<string name="staking_reward_schedule_each_day">Each day</string>
<string name="staking_reward_schedule_epoch">Epoch</string>
<string name="staking_reward_schedule_era">Era</string>
<string name="staking_reward_schedule_hour">Hour</string>

View file

@ -1,10 +1,7 @@
package com.tangem.core.ui.components.marketprice
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -26,6 +23,11 @@ fun PriceChangeInPercent(
modifier: Modifier = Modifier,
textStyle: TextStyle = TangemTheme.typography.body2,
) {
if (valueInPercent.isBlank()) {
Box(modifier)
return
}
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,

View file

@ -25,6 +25,13 @@ object BigDecimalFormatter {
private const val FIAT_MARKET_DEFAULT_DIGITS = 2
private const val FIAT_MARKET_EXTENDED_DIGITS = 6
private val bigDecimal01 = BigDecimal("0.1")
private val bigDecimal001 = BigDecimal("0.01")
private val bigDecimal0001 = BigDecimal("0.001")
private val bigDecimal00001 = BigDecimal("0.0001")
private val bigDecimal000001 = BigDecimal("0.00001")
private val bigDecimal0000001 = BigDecimal("0.000001")
fun formatCryptoAmount(
cryptoAmount: BigDecimal?,
cryptoCurrency: String,
@ -169,6 +176,28 @@ object BigDecimalFormatter {
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
}
fun formatFiatPriceUncapped(
fiatAmount: BigDecimal?,
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
locale: Locale = Locale.getDefault(),
): String {
if (fiatAmount == null) return EMPTY_BALANCE_SIGN
val formatterCurrency = getCurrency(fiatCurrencyCode)
val decimals = getProperFiatPriceDecimals(fiatAmount)
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
currency = formatterCurrency
maximumFractionDigits = decimals
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
roundingMode = RoundingMode.HALF_UP
}
return formatter.format(fiatAmount)
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
}
fun formatFiatEditableAmount(
fiatAmount: String?,
fiatCurrencyCode: String,
@ -330,4 +359,18 @@ object BigDecimalFormatter {
}
private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
@Suppress("MagicNumber")
fun getProperFiatPriceDecimals(price: BigDecimal): Int {
return when {
price >= BigDecimal.ONE -> 2
price >= bigDecimal01 -> 3
price >= bigDecimal001 -> 4
price >= bigDecimal0001 -> 6
price >= bigDecimal00001 -> 8
price >= bigDecimal000001 -> 10
price >= bigDecimal0000001 -> 12
else -> price.stripTrailingZeros().scale()
}
}
}

View file

@ -10,8 +10,6 @@ import java.util.Locale
@Suppress("MagicNumber")
object DateTimeFormatters {
private const val DDMMYYYY = "dd.MM.yyyy"
/**
* Two SS means, SHORT style for date and time.
* If pattern contains "a", it means time is in 12 hour format.
@ -51,7 +49,7 @@ object DateTimeFormatters {
val dateDDMMYYYY: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendPattern(DDMMYYYY)
.appendPattern("dd.MM.yyyy")
.toFormatter()
.withLocale(Locale.getDefault())
}
@ -59,18 +57,12 @@ object DateTimeFormatters {
/**
* In API version < 24, there may be some problems with getting the best date and time format pattern.
*/
val dateMMMMd: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "dd MMM"))
.toFormatter()
.withLocale(Locale.getDefault())
val dateMMMdd: DateTimeFormatter by lazy {
getBestFormatterBySkeleton("dd MMM")
}
val dateYYYY: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyy"))
.toFormatter()
.withLocale(Locale.getDefault())
getBestFormatterBySkeleton("yyyy")
}
val dateTimeFormatter: DateTimeFormatter by lazy {
@ -80,4 +72,11 @@ object DateTimeFormatters {
fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String {
return formatter.print(date)
}
fun getBestFormatterBySkeleton(skeleton: String): DateTimeFormatter {
return DateTimeFormatterBuilder()
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton))
.toFormatter()
.withLocale(Locale.getDefault())
}
}

View file

@ -29,5 +29,12 @@ fun Long.toDateFormatWithTodayYesterday(formatter: DateTimeFormatter = DateTimeF
* Returns formatted time according to [formatter].
*/
fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeFormatter): String {
return formatAsDateTime(formatter)
}
/**
* Returns formatted date-time according to [formatter].
*/
fun Long.formatAsDateTime(formatter: DateTimeFormatter): String {
return DateTimeFormatters.formatDate(date = DateTime(this, DateTimeZone.getDefault()), formatter = formatter)
}

View file

@ -19,9 +19,6 @@ internal class DefaultMarketsTokenRepository(
private val dispatcherProvider: CoroutineDispatcherProvider,
) : MarketsTokenRepository {
private val tokenListConverter = TokenMarketListConverter()
private val tokenChartConverter = TokenChartConverter()
private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher(
prefetchDistance = firstBatchSize,
batchSize = nextBatchSize,
@ -65,7 +62,7 @@ internal class DefaultMarketsTokenRepository(
val last = res.tokens.size < request.limit
return BatchFetchResult.Success(
data = tokenListConverter.convert(res),
data = TokenMarketListConverter.convert(res),
last = last,
empty = res.tokens.isEmpty(),
)
@ -105,7 +102,7 @@ internal class DefaultMarketsTokenRepository(
interval = interval.toRequestParam(),
)
return tokenChartConverter.convert(interval, response.getOrThrow())
return TokenChartConverter.convert(interval, response.getOrThrow())
}
override suspend fun getTokenInfo(
@ -119,6 +116,17 @@ internal class DefaultMarketsTokenRepository(
language = languageCode,
)
return TokenMarketInfoConverter().convert(response.getOrThrow())
return TokenMarketInfoConverter.convert(response.getOrThrow())
}
override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes {
// TODO change method when backend is ready
val response = marketsApi.getCoinMarketData(
currency = fiatCurrencyCode,
coinId = tokenId,
language = "en",
)
return TokenMarketInfoConverter.convert(response.getOrThrow()).quotes
}
}

View file

@ -1,8 +1,7 @@
package com.tangem.data.markets
import com.tangem.data.markets.converters.TokenChartConverter
import com.tangem.data.markets.converters.TokenMarketChartsConverter
import com.tangem.data.markets.converters.TokenQuotesConverter
import com.tangem.data.markets.converters.TokenQuotesShortConverter
import com.tangem.data.markets.converters.toRequestParam
import com.tangem.data.markets.utils.retryOnError
import com.tangem.datasource.api.common.response.getOrThrow
@ -21,9 +20,6 @@ internal class MarketsBatchUpdateFetcher(
private val tangemTechApi: TangemTechApi,
) : BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
private val tokenListChartsConverter = TokenMarketChartsConverter(TokenChartConverter())
private val tokenQuotesConverter = TokenQuotesConverter()
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.fetchUpdateAsync(
toUpdate: List<Batch<Int, List<TokenMarket>>>,
updateRequest: TokenMarketUpdateRequest,
@ -75,7 +71,7 @@ internal class MarketsBatchUpdateFetcher(
val res = toUpdate.map { batch ->
batch.copy(
data = batch.data.map {
it.copy(tokenQuotes = tokenQuotesConverter.convert(it.id, quotesRes))
it.copy(tokenQuotesShort = TokenQuotesShortConverter.convert(it.id, quotesRes))
},
)
}
@ -97,7 +93,7 @@ internal class MarketsBatchUpdateFetcher(
key = batchToUpdate.key,
data = batchToUpdate.data.map {
it.copy(
tokenCharts = tokenListChartsConverter.convert(
tokenCharts = TokenMarketChartsConverter.convert(
chartsToCopy = it.tokenCharts,
tokenId = it.id,
interval = updateRequest.interval,

View file

@ -4,7 +4,7 @@ import com.tangem.datasource.api.markets.models.response.TokenMarketChartRespons
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenChart
class TokenChartConverter {
internal object TokenChartConverter {
fun convert(interval: PriceChangeInterval, value: TokenMarketChartResponse): TokenChart {
return TokenChart(

View file

@ -3,13 +3,13 @@ package com.tangem.data.markets.converters
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketListConfig
fun TokenMarketListConfig.Interval.toRequestParam(): String = when (this) {
internal fun TokenMarketListConfig.Interval.toRequestParam(): String = when (this) {
TokenMarketListConfig.Interval.H24 -> "24h"
TokenMarketListConfig.Interval.WEEK -> "1w"
TokenMarketListConfig.Interval.MONTH -> "30d"
}
fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) {
internal fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) {
TokenMarketListConfig.Order.ByRating -> "rating"
TokenMarketListConfig.Order.Trending -> "trending"
TokenMarketListConfig.Order.Buyers -> "buyers"
@ -17,7 +17,7 @@ fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) {
TokenMarketListConfig.Order.TopLosers -> "losers"
}
fun PriceChangeInterval.toRequestParam(): String = when (this) {
internal fun PriceChangeInterval.toRequestParam(): String = when (this) {
PriceChangeInterval.H24 -> "24h"
PriceChangeInterval.WEEK -> "1w"
PriceChangeInterval.MONTH -> "1m"

View file

@ -5,9 +5,7 @@ import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.TokenMarketListConfig
class TokenMarketChartsConverter(
private val tokenChartConverter: TokenChartConverter,
) {
internal object TokenMarketChartsConverter {
fun convert(
chartsToCopy: TokenMarket.Charts,
@ -20,13 +18,13 @@ class TokenMarketChartsConverter(
}
return when (interval) {
TokenMarketListConfig.Interval.H24 -> chartsToCopy.copy(
h24 = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
h24 = TokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
)
TokenMarketListConfig.Interval.WEEK -> chartsToCopy.copy(
week = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
week = TokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
)
TokenMarketListConfig.Interval.MONTH -> chartsToCopy.copy(
month = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
month = TokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
)
else -> error("unsupported interval=$interval. This shouldn't have happened.")
}

View file

@ -2,9 +2,10 @@ package com.tangem.data.markets.converters
import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenQuotes
import com.tangem.utils.converter.Converter
internal class TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, TokenMarketInfo> {
internal object TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, TokenMarketInfo> {
override fun convert(value: TokenMarketInfoResponse): TokenMarketInfo {
return with(value) {
@ -12,8 +13,7 @@ internal class TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, Tok
id = id,
name = name,
symbol = symbol,
currentPrice = currentPrice,
priceChangePercentage = priceChangePercentage?.convert(),
quotes = getQuotes(),
networks = networks?.convert(),
shortDescription = shortDescription,
fullDescription = fullDescription,
@ -25,15 +25,16 @@ internal class TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, Tok
}
}
private fun TokenMarketInfoResponse.PriceChangePercentage.convert(): TokenMarketInfo.PriceChangePercentage {
return TokenMarketInfo.PriceChangePercentage(
day = day,
week = week,
month = month,
threeMonths = threeMonths,
sixMonths = sixMonths,
year = year,
allTime = allTime,
private fun TokenMarketInfoResponse.getQuotes(): TokenQuotes {
return TokenQuotes(
currentPrice = currentPrice,
h24ChangePercent = priceChangePercentage?.day?.movePointLeft(2),
weekChangePercent = priceChangePercentage?.week?.movePointLeft(2),
monthChangePercent = priceChangePercentage?.month?.movePointLeft(2),
m3ChangePercent = priceChangePercentage?.threeMonths?.movePointLeft(2),
m6ChangePercent = priceChangePercentage?.sixMonths?.movePointLeft(2),
yearChangePercent = priceChangePercentage?.year?.movePointLeft(2),
allTimeChangePercent = priceChangePercentage?.allTime?.movePointLeft(2),
)
}

View file

@ -1,12 +1,11 @@
package com.tangem.data.markets.converters
import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.TokenQuotes
import com.tangem.domain.markets.TokenQuotesShort
import com.tangem.utils.converter.Converter
class TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMarket>> {
internal object TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMarket>> {
override fun convert(value: TokenMarketListResponse): List<TokenMarket> {
val imageHost = value.imageHost ?: run {
@ -25,13 +24,11 @@ class TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMa
marketRating = token.marketRating,
marketCap = token.marketCap,
imageHost = imageHost,
tokenQuotes = TokenQuotes(
tokenQuotesShort = TokenQuotesShort(
currentPrice = token.currentPrice,
priceChanges = mapOf(
PriceChangeInterval.H24 to token.priceChangePercentage.h24.movePointLeft(2),
PriceChangeInterval.WEEK to token.priceChangePercentage.week1.movePointLeft(2),
PriceChangeInterval.MONTH to token.priceChangePercentage.day30.movePointLeft(2),
),
h24ChangePercent = token.priceChangePercentage.h24.movePointLeft(2),
weekChangePercent = token.priceChangePercentage.week1.movePointLeft(2),
monthChangePercent = token.priceChangePercentage.day30.movePointLeft(2),
),
tokenCharts = TokenMarket.Charts(null, null, null),
)

View file

@ -1,25 +1,22 @@
package com.tangem.data.markets.converters
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenQuotes
import com.tangem.domain.markets.TokenQuotesShort
import java.math.BigDecimal
class TokenQuotesConverter {
internal object TokenQuotesShortConverter {
fun convert(tokenId: String, value: QuotesResponse): TokenQuotes {
fun convert(tokenId: String, value: QuotesResponse): TokenQuotesShort {
val quote = requireNotNull(value.quotes[tokenId]) {
"$tokenId is not found in the response. This shouldn't have happened."
}
return TokenQuotes(
return TokenQuotesShort(
currentPrice = requireNotNull(quote.price) {
"Price is not found in the QuotesResponse. This shouldn't have happened."
},
priceChanges = mapOf(
PriceChangeInterval.H24 to (quote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2),
PriceChangeInterval.WEEK to (quote.priceChange1w ?: BigDecimal.ZERO).movePointLeft(2),
PriceChangeInterval.MONTH to (quote.priceChange30d ?: BigDecimal.ZERO).movePointLeft(2),
),
h24ChangePercent = (quote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2),
weekChangePercent = (quote.priceChange1w ?: BigDecimal.ZERO).movePointLeft(2),
monthChangePercent = (quote.priceChange30d ?: BigDecimal.ZERO).movePointLeft(2),
)
}
}

View file

@ -8,7 +8,7 @@ data class TokenMarket(
val symbol: String,
val marketRating: Int?,
val marketCap: BigDecimal?,
val tokenQuotes: TokenQuotes,
val tokenQuotesShort: TokenQuotesShort,
val tokenCharts: Charts,
private val imageHost: String,
) {

View file

@ -6,8 +6,7 @@ data class TokenMarketInfo(
val id: String,
val name: String,
val symbol: String,
val currentPrice: BigDecimal,
val priceChangePercentage: PriceChangePercentage?,
val quotes: TokenQuotes,
val networks: List<Network>?,
val shortDescription: String?,
val fullDescription: String?,
@ -16,16 +15,6 @@ data class TokenMarketInfo(
val links: Links?,
val pricePerformance: PricePerformance?,
) {
data class PriceChangePercentage(
val day: BigDecimal?,
val week: BigDecimal?,
val month: BigDecimal?,
val threeMonths: BigDecimal?,
val sixMonths: BigDecimal?,
val year: BigDecimal?,
val allTime: BigDecimal?,
)
data class Network(
val networkId: String,
val exchangeable: Boolean,

View file

@ -4,15 +4,11 @@ import java.math.BigDecimal
data class TokenQuotes(
val currentPrice: BigDecimal,
private val priceChanges: Map<PriceChangeInterval, BigDecimal>,
) {
init {
require(priceChanges.containsKey(PriceChangeInterval.H24))
require(priceChanges.containsKey(PriceChangeInterval.WEEK))
require(priceChanges.containsKey(PriceChangeInterval.MONTH))
}
fun h24Percent() = priceChanges[PriceChangeInterval.H24]!!
fun weekPercent() = priceChanges[PriceChangeInterval.WEEK]!!
fun monthPercent() = priceChanges[PriceChangeInterval.MONTH]!!
}
val h24ChangePercent: BigDecimal?,
val weekChangePercent: BigDecimal?,
val monthChangePercent: BigDecimal?,
val m3ChangePercent: BigDecimal?,
val m6ChangePercent: BigDecimal?,
val yearChangePercent: BigDecimal?,
val allTimeChangePercent: BigDecimal?,
)

View file

@ -0,0 +1,10 @@
package com.tangem.domain.markets
import java.math.BigDecimal
data class TokenQuotesShort(
val currentPrice: BigDecimal,
val h24ChangePercent: BigDecimal,
val weekChangePercent: BigDecimal,
val monthChangePercent: BigDecimal,
)

View file

@ -0,0 +1,19 @@
package com.tangem.domain.markets
import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.repositories.MarketsTokenRepository
class GetTokenQuotesUseCase(
private val marketsTokenRepository: MarketsTokenRepository,
) {
suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either<Unit, TokenQuotes> {
return Either.catch {
marketsTokenRepository.getTokenQuotes(
fiatCurrencyCode = appCurrency.code,
tokenId = tokenId,
)
}.mapLeft {}
}
}

View file

@ -13,4 +13,6 @@ interface MarketsTokenRepository {
suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String, languageCode: String): TokenMarketInfo
suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes
}

View file

@ -30,10 +30,10 @@ fun TokenMarket.toSerializable(): TokenMarketSerializable {
symbol = symbol,
marketCap = marketCap,
tokenQuotes = TokenMarketSerializable.Quotes(
currentPrice = tokenQuotes.currentPrice,
h24Percent = tokenQuotes.h24Percent(),
weekPercent = tokenQuotes.weekPercent(),
monthPercent = tokenQuotes.monthPercent(),
currentPrice = tokenQuotesShort.currentPrice,
h24Percent = tokenQuotesShort.h24ChangePercent,
weekPercent = tokenQuotesShort.weekChangePercent,
monthPercent = tokenQuotesShort.monthChangePercent,
),
imageUrl = imageUrlLarge,
)

View file

@ -1,11 +1,9 @@
package com.tangem.features.markets.details.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -32,11 +30,23 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
) {
LifecycleStartEffect(Unit) {
model.isVisibleOnScreen.value = true
onStopOrDispose {
model.isVisibleOnScreen.value = false
}
}
val state by model.state.collectAsStateWithLifecycle()
val bsState by bottomSheetState
LaunchedEffect(bsState) {
model.containerBottomSheetState.value = bsState
}
MarketsTokenDetailsContent(
state = state,
onBackClick = { onBack() },
onBackClick = onBack,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)

View file

@ -9,19 +9,22 @@ import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
import com.tangem.domain.markets.GetTokenPriceChartUseCase
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.*
import com.tangem.features.markets.component.BottomSheetState
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter
import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter
import com.tangem.features.markets.details.impl.model.formatter.*
import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice
import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween
import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.impl.R
@ -30,13 +33,15 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.joda.time.DateTime
import java.math.BigDecimal
import java.math.RoundingMode
import javax.inject.Inject
@Suppress("LargeClass")
@Suppress("LargeClass", "LongParameterList")
@Stable
internal class MarketsTokenDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
@ -44,10 +49,12 @@ internal class MarketsTokenDetailsModel @Inject constructor(
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
private val getTokenQuotesUseCase: GetTokenQuotesUseCase,
private val urlOpener: UrlOpener,
) : Model() {
val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>()
private var quotesJob = JobHolder()
private val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>()
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
@ -77,13 +84,13 @@ internal class MarketsTokenDetailsModel @Inject constructor(
chartData = MarketChartData.NoData.Loading
updateLook {
val percentChangeType = params.token.tokenQuotes.h24Percent.percentChangeType()
it.copy(
type = getChartTypeByPercent(params.token.tokenQuotes.h24Percent),
xAxisFormatter = { value ->
value.toLong().toTimeFormat(DateTimeFormatters.timeFormatter)
},
type = percentChangeType.toChartType(),
xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24),
yAxisFormatter = { value ->
BigDecimalFormatter.formatFiatAmountUncapped(
BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = value,
fiatCurrencyCode = currentAppCurrency.value.code,
fiatCurrencySymbol = "",
@ -93,10 +100,28 @@ internal class MarketsTokenDetailsModel @Inject constructor(
}
}
private val currentQuotes = MutableStateFlow(
TokenQuotes(
currentPrice = params.token.tokenQuotes.currentPrice,
h24ChangePercent = params.token.tokenQuotes.h24Percent,
weekChangePercent = params.token.tokenQuotes.weekPercent,
monthChangePercent = params.token.tokenQuotes.monthPercent,
m3ChangePercent = null,
m6ChangePercent = null,
yearChangePercent = null,
allTimeChangePercent = null,
),
)
private var lastUpdatedTimestamp: Long = DateTime.now().millis
val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED)
val isVisibleOnScreen = MutableStateFlow(false)
val state = MutableStateFlow(
MarketsTokenDetailsUM(
tokenName = params.token.name,
priceText = BigDecimalFormatter.formatFiatAmountUncapped(
priceText = BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = params.token.tokenQuotes.currentPrice,
fiatCurrencyCode = currentAppCurrency.value.code,
fiatCurrencySymbol = currentAppCurrency.value.symbol,
@ -121,7 +146,9 @@ internal class MarketsTokenDetailsModel @Inject constructor(
),
selectedInterval = PriceChangeInterval.H24,
onSelectedIntervalChange = ::onSelectedIntervalChange,
markerSet = false,
body = MarketsTokenDetailsUM.Body.Loading,
triggerPriceChange = consumedEvent(),
infoBottomSheet = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
@ -146,21 +173,22 @@ internal class MarketsTokenDetailsModel @Inject constructor(
}
private fun initialLoad() {
loadChart(state.value.selectedInterval)
loadInfo()
loadChart(state.value.selectedInterval)
modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS)
}
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
if (state.value.selectedInterval == interval) return
state.update {
it.copy(
selectedInterval = interval,
priceChangeType = PriceChangeType.UP,
private fun loadQuotes() {
modelScope.launch {
val result = getTokenQuotesUseCase(
tokenId = params.token.id,
appCurrency = currentAppCurrency.value,
)
}
loadChart(interval)
result.onRight { res ->
updateQuotes(res)
}
}
}
private fun loadChart(interval: PriceChangeInterval) {
@ -192,7 +220,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
)
}
val xAxisFormatter = getFormatterByInterval(state.value.selectedInterval)
val xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(state.value.selectedInterval)
chart.onRight {
chartDataProducer.runTransactionSuspend {
@ -204,6 +232,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
updateLook {
it.copy(
xAxisFormatter = xAxisFormatter,
type = state.value.priceChangeType.toChartType(),
)
}
}
@ -247,8 +276,15 @@ internal class MarketsTokenDetailsModel @Inject constructor(
tokenMarketInfo.fold(
ifRight = { result ->
currentQuotes.value = result.quotes
val percent = result.quotes.getPercentByInterval(interval = state.value.selectedInterval)
state.update {
it.copy(
priceText = result.quotes.currentPrice.formatAsPrice(currentAppCurrency.value),
priceChangePercentText = result.quotes.getFormattedPercentByInterval(
interval = it.selectedInterval,
),
priceChangeType = percent.percentChangeType(),
body = MarketsTokenDetailsUM.Body.Content(
description = descriptionConverter.convert(result),
infoBlocks = infoConverter.convert(result),
@ -275,57 +311,101 @@ internal class MarketsTokenDetailsModel @Inject constructor(
}
}
private fun getFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
return when (interval) {
PriceChangeInterval.H24 -> { value: BigDecimal ->
value.toLong().toTimeFormat(DateTimeFormatters.timeFormatter)
}
PriceChangeInterval.WEEK,
PriceChangeInterval.MONTH,
PriceChangeInterval.MONTH3,
PriceChangeInterval.MONTH6,
-> { value ->
value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd)
}
PriceChangeInterval.YEAR -> { value ->
value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd)
}
PriceChangeInterval.ALL_TIME -> { value ->
value.toLong().toTimeFormat(DateTimeFormatters.dateYYYY)
}
private suspend fun updateQuotes(newQuotes: TokenQuotes) {
val triggerPriceChangeType = getFormattedPriceChange(
currentPrice = currentQuotes.value.currentPrice,
updatedPrice = newQuotes.currentPrice,
)
val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) {
triggeredEvent(
data = triggerPriceChangeType,
onConsume = {
state.update { it.copy(triggerPriceChange = consumedEvent()) }
},
)
} else {
consumedEvent()
}
val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval)
val priceChangeType = percent.percentChangeType()
// wait until marker is removed
state.first { it.markerSet.not() }
currentQuotes.value = newQuotes
lastUpdatedTimestamp = DateTime.now().millis
state.update { stateToUpdate ->
stateToUpdate.copy(
priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency.value),
priceChangePercentText = newQuotes.getFormattedPercentByInterval(
interval = stateToUpdate.selectedInterval,
),
priceChangeType = priceChangeType,
triggerPriceChange = trigger,
dateTimeText = getDefaultDateTimeString(stateToUpdate.selectedInterval),
)
}
}
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
if (state.value.selectedInterval == interval) return
val quotes = currentQuotes.value
val priceChangePercent = quotes.getFormattedPercentByInterval(interval)
state.update {
it.copy(
priceChangePercentText = priceChangePercent,
selectedInterval = interval,
priceChangeType = quotes.getPercentByInterval(interval)?.percentChangeType()
?: PriceChangeType.NEUTRAL,
dateTimeText = getDefaultDateTimeString(interval),
)
}
loadChart(interval)
if (priceChangePercent.isEmpty()) {
loadQuotes()
}
}
@Suppress("MagicNumber")
private fun onMarkerPointSelected(time: BigDecimal?, price: BigDecimal?) {
val timeText = time?.toLong()?.toTimeFormat(DateTimeFormatters.dateTimeFormatter)?.let {
resourceReference(R.string.common_range, wrappedList(it, resourceReference(R.string.common_now)))
} ?: resourceReference(R.string.common_today)
private fun onMarkerPointSelected(markerTimestamp: BigDecimal?, price: BigDecimal?) {
val currentState = state.value
val percent = price?.subtract(params.token.tokenQuotes.currentPrice)
?.divide(params.token.tokenQuotes.currentPrice, 4, RoundingMode.HALF_UP)
?.multiply(BigDecimal(-100))
?: params.token.tokenQuotes.h24Percent
val dateTimeText = markerTimestamp?.let {
MarketsDateTimeFormatters.formatDateByIntervalWithMarker(
interval = currentState.selectedInterval,
markerTimestamp = it,
)
} ?: getDefaultDateTimeString(currentState.selectedInterval)
val percentText = BigDecimalFormatter.formatPercent(
percent = percent,
useAbsoluteValue = true,
)
val priceText = (price ?: currentQuotes.value.currentPrice).formatAsPrice(currentAppCurrency.value)
state.update {
it.copy(
dateTimeText = timeText,
priceText = BigDecimalFormatter.formatFiatAmountUncapped(
fiatAmount = price ?: params.token.tokenQuotes.currentPrice,
fiatCurrencyCode = currentAppCurrency.value.code,
fiatCurrencySymbol = currentAppCurrency.value.symbol,
),
val percent = price?.let {
getChangePercentBetween(
previousPrice = it,
currentPrice = currentQuotes.value.currentPrice,
)
} ?: currentQuotes.value.getPercentByInterval(currentState.selectedInterval)
val percentText = percent?.let {
BigDecimalFormatter.formatPercent(
percent = it,
useAbsoluteValue = true,
)
} ?: ""
state.update { stateToUpdate ->
stateToUpdate.copy(
markerSet = markerTimestamp != null,
dateTimeText = dateTimeText,
priceText = priceText,
priceChangePercentText = percentText,
priceChangeType = when {
percent < BigDecimal.ZERO -> PriceChangeType.DOWN
percent > BigDecimal.ZERO -> PriceChangeType.UP
else -> PriceChangeType.NEUTRAL
},
priceChangeType = percent.percentChangeType(),
)
}
@ -338,14 +418,6 @@ internal class MarketsTokenDetailsModel @Inject constructor(
}
}
private fun getChartTypeByPercent(percent: BigDecimal): MarketChartLook.Type {
return if (percent >= BigDecimal.ZERO) {
MarketChartLook.Type.Growing
} else {
MarketChartLook.Type.Falling
}
}
private fun showInfoBottomSheet(content: InfoBottomSheetContent) {
state.update { stateToUpdate ->
stateToUpdate.copy(
@ -379,6 +451,35 @@ internal class MarketsTokenDetailsModel @Inject constructor(
currentState.body is MarketsTokenDetailsUM.Body.Nothing
) {
loadInfo()
modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS)
}
}
private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
launch {
while (true) {
delay(timeMillis)
// Update quotes only when the container bottom sheet is in the expanded state
containerBottomSheetState.first { it == BottomSheetState.EXPANDED }
// and is visible on the screen
isVisibleOnScreen.first { it }
loadQuotes()
}
}.saveIn(quotesJob)
}
private fun getDefaultDateTimeString(interval: PriceChangeInterval): TextReference {
return MarketsDateTimeFormatters.formatDateByInterval(
interval = interval,
startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval(
interval = interval,
currentTimestamp = lastUpdatedTimestamp,
),
)
}
private companion object {
const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L
}
}

View file

@ -42,8 +42,9 @@ internal class PricePerformanceConverter(
private fun BigDecimal?.convert(): String {
val currency = appCurrency()
return BigDecimalFormatter.formatCompactFiatAmount(
amount = this,
return BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = this,
fiatCurrencyCode = currency.code,
fiatCurrencySymbol = currency.symbol,
)

View file

@ -0,0 +1,98 @@
package com.tangem.features.markets.details.impl.model.formatter
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenQuotes
import java.math.BigDecimal
import java.math.RoundingMode
internal fun BigDecimal.formatAsPrice(currency: AppCurrency): String {
return BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = this,
fiatCurrencyCode = currency.code,
fiatCurrencySymbol = currency.symbol,
)
}
internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInterval): String {
val percent = when (interval) {
PriceChangeInterval.H24 -> h24ChangePercent
PriceChangeInterval.WEEK -> weekChangePercent
PriceChangeInterval.MONTH -> monthChangePercent
PriceChangeInterval.MONTH3 -> m3ChangePercent
PriceChangeInterval.MONTH6 -> m6ChangePercent
PriceChangeInterval.YEAR -> yearChangePercent
PriceChangeInterval.ALL_TIME -> allTimeChangePercent
}
return percent?.let {
BigDecimalFormatter.formatPercent(
percent = it,
useAbsoluteValue = true,
)
} ?: ""
}
internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): BigDecimal? {
return when (interval) {
PriceChangeInterval.H24 -> h24ChangePercent
PriceChangeInterval.WEEK -> weekChangePercent
PriceChangeInterval.MONTH -> monthChangePercent
PriceChangeInterval.MONTH3 -> m3ChangePercent
PriceChangeInterval.MONTH6 -> m6ChangePercent
PriceChangeInterval.YEAR -> yearChangePercent
PriceChangeInterval.ALL_TIME -> allTimeChangePercent
}
}
internal fun BigDecimal?.percentChangeType(): PriceChangeType {
return when {
this == null -> PriceChangeType.NEUTRAL
this > BigDecimal.ZERO -> PriceChangeType.UP
this < BigDecimal.ZERO -> PriceChangeType.DOWN
else -> PriceChangeType.NEUTRAL
}
}
@Suppress("MagicNumber")
internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: BigDecimal): BigDecimal {
return if (previousPrice == BigDecimal.ZERO) {
BigDecimal.ZERO
} else {
currentPrice.subtract(previousPrice).divide(previousPrice, 4, RoundingMode.HALF_UP)
}
}
internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType {
val updatedPriceDecimals = BigDecimalFormatter.getProperFiatPriceDecimals(updatedPrice)
val current = currentPrice.setScale(updatedPriceDecimals, RoundingMode.HALF_UP)
val updated = updatedPrice.setScale(updatedPriceDecimals, RoundingMode.HALF_UP)
return when {
updated > current -> PriceChangeType.UP
updated < current -> PriceChangeType.DOWN
else -> PriceChangeType.NEUTRAL
}
}
internal fun PriceChangeType.toChartType(): MarketChartLook.Type {
return when (this) {
PriceChangeType.UP -> MarketChartLook.Type.Growing
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Growing
}
}
internal fun getChartTypeByPercent(percent: BigDecimal?): MarketChartLook.Type {
return when {
percent == null -> return MarketChartLook.Type.Growing
percent == BigDecimal.ZERO -> MarketChartLook.Type.Growing
percent > BigDecimal.ZERO -> MarketChartLook.Type.Growing
percent < BigDecimal.ZERO -> MarketChartLook.Type.Falling
else -> MarketChartLook.Type.Growing
}
}

View file

@ -0,0 +1,130 @@
package com.tangem.features.markets.details.impl.model.formatter
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.formatAsDateTime
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.features.markets.impl.R
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import java.math.BigDecimal
internal object MarketsDateTimeFormatters {
private const val H24_MILLIS = 24L * 60 * 60 * 1000
private const val WEEK_MILLIS = 7L * H24_MILLIS
private val dateTimeMMMFormatter by lazy {
DateTimeFormatters.getBestFormatterBySkeleton("dd MMM hh:mm")
}
private val dateFormatter = DateTimeFormatters.dateDDMMYYYY
internal fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
return when (interval) {
PriceChangeInterval.H24 -> { value: BigDecimal ->
value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter)
}
PriceChangeInterval.WEEK,
PriceChangeInterval.MONTH,
PriceChangeInterval.MONTH3,
PriceChangeInterval.MONTH6,
-> { value ->
value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd)
}
PriceChangeInterval.YEAR -> { value ->
value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd)
}
PriceChangeInterval.ALL_TIME -> { value ->
value.toLong().formatAsDateTime(DateTimeFormatters.dateYYYY)
}
}
}
internal fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference {
return when (interval) {
PriceChangeInterval.H24 -> resourceReference(R.string.common_today)
PriceChangeInterval.WEEK,
PriceChangeInterval.MONTH,
PriceChangeInterval.MONTH3,
-> {
resourceReference(
R.string.common_range_with_space,
wrappedList(
stringReference(
startTimestamp.formatAsDateTime(MarketsDateTimeFormatters.dateTimeMMMFormatter),
),
resourceReference(R.string.common_now),
),
)
}
PriceChangeInterval.MONTH6,
PriceChangeInterval.YEAR,
-> {
resourceReference(
R.string.common_range_with_space,
wrappedList(
stringReference(
startTimestamp.formatAsDateTime(MarketsDateTimeFormatters.dateFormatter),
),
resourceReference(R.string.common_now),
),
)
}
PriceChangeInterval.ALL_TIME -> resourceReference(R.string.common_all)
}
}
internal fun formatDateByIntervalWithMarker(
interval: PriceChangeInterval,
markerTimestamp: BigDecimal,
): TextReference {
return when (interval) {
PriceChangeInterval.H24,
PriceChangeInterval.WEEK,
PriceChangeInterval.MONTH,
PriceChangeInterval.MONTH3,
-> {
resourceReference(
R.string.common_range_with_space,
wrappedList(
stringReference(
markerTimestamp.toLong().formatAsDateTime(MarketsDateTimeFormatters.dateTimeMMMFormatter),
),
resourceReference(R.string.common_now),
),
)
}
PriceChangeInterval.MONTH6,
PriceChangeInterval.YEAR,
PriceChangeInterval.ALL_TIME,
-> {
resourceReference(
R.string.common_range_with_space,
wrappedList(
stringReference(
markerTimestamp.toLong().formatAsDateTime(MarketsDateTimeFormatters.dateFormatter),
),
resourceReference(R.string.common_now),
),
)
}
}
}
@Suppress("MagicNumber")
fun getStartTimestampByInterval(interval: PriceChangeInterval, currentTimestamp: Long): Long {
return when (interval) {
PriceChangeInterval.H24 -> currentTimestamp - H24_MILLIS
PriceChangeInterval.WEEK -> currentTimestamp - WEEK_MILLIS
PriceChangeInterval.MONTH -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(1).millis
PriceChangeInterval.MONTH3 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(3).millis
PriceChangeInterval.MONTH6 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(6).millis
PriceChangeInterval.YEAR -> DateTime(currentTimestamp, DateTimeZone.UTC).minusYears(1).millis
PriceChangeInterval.ALL_TIME -> 0
}
}
}

View file

@ -1,10 +1,13 @@
package com.tangem.features.markets.details.impl.ui
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@ -23,6 +26,9 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.currency.icon.CoinIcon
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
@ -134,11 +140,12 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier)
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column {
Text(
text = state.priceText,
style = TangemTheme.typography.head,
color = TangemTheme.colors.text.primary1,
Column(
Modifier.weight(1f),
) {
TokenPriceText(
price = state.priceText,
triggerPriceChange = state.triggerPriceChange,
)
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
Text(
@ -155,7 +162,7 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier)
}
SpacerW4()
CoinIcon(
modifier = Modifier.size(TangemTheme.dimens.size48),
modifier = Modifier.requiredSize(TangemTheme.dimens.size48),
url = state.iconUrl,
alpha = 1f,
colorFilter = null,
@ -164,6 +171,38 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier)
}
}
@Composable
private fun TokenPriceText(
price: String,
triggerPriceChange: StateEvent<PriceChangeType>,
modifier: Modifier = Modifier,
) {
val growColor = TangemTheme.colors.text.accent
val fallColor = TangemTheme.colors.text.warning
val generalColor = TangemTheme.colors.text.primary1
val color = remember { Animatable(generalColor) }
EventEffect(triggerPriceChange) {
val nextColor = when (it) {
PriceChangeType.UP,
-> growColor
PriceChangeType.DOWN -> fallColor
PriceChangeType.NEUTRAL -> return@EventEffect
}
color.animateTo(nextColor, snap())
color.animateTo(generalColor, tween(durationMillis = 500))
}
Text(
modifier = modifier,
text = price,
color = color.value,
style = TangemTheme.typography.head,
)
}
@Composable
private fun IntervalSelector(
trendInterval: PriceChangeInterval,
@ -224,9 +263,9 @@ private fun Preview() {
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
state = MarketsTokenDetailsUM(
tokenName = "Token Name",
priceText = "Price",
dateTimeText = stringReference("Date Time"),
priceChangePercentText = "Price Change",
priceText = "$0.00000000324",
dateTimeText = stringReference("Today"),
priceChangePercentText = "52.00%",
iconUrl = "",
priceChangeType = PriceChangeType.UP,
chartState = MarketsTokenDetailsUM.ChartState(
@ -244,6 +283,8 @@ private fun Preview() {
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
markerSet = false,
triggerPriceChange = consumedEvent(),
),
onHeaderSizeChange = {},
onBackClick = {},

View file

@ -1,7 +1,6 @@
package com.tangem.features.markets.details.impl.ui.components
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text

View file

@ -5,6 +5,7 @@ import com.tangem.common.ui.charts.state.MarketChartDataProducer
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.markets.PriceChangeInterval
import java.math.BigDecimal
@ -17,9 +18,11 @@ internal data class MarketsTokenDetailsUM(
val priceChangePercentText: String,
val priceChangeType: PriceChangeType,
val selectedInterval: PriceChangeInterval,
val markerSet: Boolean,
val chartState: ChartState,
val onSelectedIntervalChange: (PriceChangeInterval) -> Unit,
val infoBottomSheet: TangemBottomSheetConfig,
val triggerPriceChange: StateEvent<PriceChangeType>,
val body: Body,
) {

View file

@ -48,17 +48,17 @@ internal class MarketsTokenItemConverter(
ratingPosition = new.marketRating?.toString(),
marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() },
iconUrl = new.imageUrlLarge,
price = ifChanged(prev = prev.tokenQuotes, new = new.tokenQuotes, prevR = prevUI.price) {
price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) {
new.getCurrentPrice(
prev = prev,
)
},
trendPercentText = ifChanged(
prev.tokenQuotes,
new.tokenQuotes,
prev.tokenQuotesShort,
new.tokenQuotesShort,
prevUI.trendPercentText,
) { new.getTrendPercent() },
trendType = ifChanged(prev.tokenQuotes, new.tokenQuotes, prevUI.trendType) { new.getTrendType() },
trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() },
chardData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chardData) { new.getChartData() },
)
}
@ -79,16 +79,16 @@ internal class MarketsTokenItemConverter(
}
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
val prevPrice = prev?.tokenQuotes?.currentPrice
val prevPrice = prev?.tokenQuotesShort?.currentPrice
val priceText = BigDecimalFormatter.formatFiatAmountUncapped(
fiatAmount = tokenQuotes.currentPrice,
val priceText = BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = tokenQuotesShort.currentPrice,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
val changeType = if (prevPrice != null) {
if (tokenQuotes.currentPrice > prevPrice) {
if (tokenQuotesShort.currentPrice > prevPrice) {
PriceChangeType.UP
} else {
PriceChangeType.DOWN
@ -122,9 +122,9 @@ internal class MarketsTokenItemConverter(
private fun TokenMarket.getTrendType(): PriceChangeType {
val percent = when (currentTrendInterval) {
TrendInterval.H24 -> tokenQuotes.h24Percent()
TrendInterval.D7 -> tokenQuotes.weekPercent()
TrendInterval.M1 -> tokenQuotes.monthPercent()
TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent
TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
}.setScale(2, RoundingMode.UP)
return when (percent.compareTo(BigDecimal.ZERO)) {
@ -136,9 +136,9 @@ internal class MarketsTokenItemConverter(
private fun TokenMarket.getTrendPercent(): String {
val percent = when (currentTrendInterval) {
TrendInterval.H24 -> tokenQuotes.h24Percent()
TrendInterval.D7 -> tokenQuotes.weekPercent()
TrendInterval.M1 -> tokenQuotes.monthPercent()
TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent
TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
}
return BigDecimalFormatter.formatPercent(

View file

@ -4,7 +4,6 @@ import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.staking.impl.R
internal sealed class StakingNotification(val config: NotificationConfig) {
@ -55,8 +54,7 @@ internal sealed class StakingNotification(val config: NotificationConfig) {
) : Warning(
title = resourceReference(R.string.staking_notification_earn_rewards_title),
subtitle = resourceReference(
R.string.staking_notification_earn_rewards_text,
wrappedList(currencyName, days),
R.string.common_error, // FIXME
),
)
}