Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-18 10:44:43 +02:00
parent 4e28463b8a
commit 546bcc908f
24 changed files with 624 additions and 131 deletions

View file

@ -7,9 +7,12 @@ internal class DateTimeAdapter : JsonAdapter<DateTime>() {
@FromJson
override fun fromJson(reader: JsonReader): DateTime? {
val dateString = reader.nextString() ?: return null
return DateTime.parse(dateString)
return if (reader.peek() == JsonReader.Token.NULL) {
reader.nextNull<DateTime>()
} else {
val dateString = reader.nextString()
DateTime.parse(dateString)
}
}
@ToJson

View file

@ -10,8 +10,12 @@ internal class LocalDateAdapter : JsonAdapter<LocalDate>() {
@FromJson
override fun fromJson(reader: JsonReader): LocalDate? {
val dateString = reader.nextString()
return LocalDate.parse(dateString, formatter)
return if (reader.peek() == JsonReader.Token.NULL) {
reader.nextNull<LocalDate>()
} else {
val dateString = reader.nextString()
return LocalDate.parse(dateString, formatter)
}
}
@ToJson

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.markets.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
@ -26,6 +27,8 @@ data class TokenMarketInfoResponse(
val insights: Insights?,
@Json(name = "metrics")
val metrics: Metrics?,
@Json(name = "security_data")
val securityData: SecurityData?,
@Json(name = "links")
val links: Links?,
@Json(name = "price_performance")
@ -144,6 +147,28 @@ data class TokenMarketInfoResponse(
val allTime: Range?,
)
@JsonClass(generateAdapter = true)
data class SecurityData(
@Json(name = "total_security_score")
val totalSecurityScore: Float,
@Json(name = "provider_data")
val providerData: List<ProviderData>,
)
@JsonClass(generateAdapter = true)
data class ProviderData(
@Json(name = "provider_id")
val providerId: String,
@Json(name = "provider_name")
val providerName: String,
@Json(name = "link")
val link: String?,
@Json(name = "security_score")
val securityScore: Float,
@Json(name = "last_audit_date")
val lastAuditDate: DateTime?,
)
@JsonClass(generateAdapter = true)
data class Range(
@Json(name = "low_price")

View file

@ -568,7 +568,7 @@
<string name="onramp_no_available_providers">Keine verfügbaren Anbieter für diese Währung</string>
<string name="onramp_pay_with">Bezahlen mit</string>
<string name="onramp_redirecting_to_provider_subtitle">Du kannst Deine Transaktion beim Drittanbieter %s abschließen.</string>
<string name="onramp_redirecting_to_provider_title">Umleitung auf %s...</string>
<string name="onramp_redirecting_to_provider_title">Umleitung auf %s</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Unsere Dienstleistungen sind in diesem Land nicht verfügbar</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Änder oder bestätige bitte</string>
<string name="onramp_residency_bottomsheet_title">Dein Wohnsitz wurde identifiziert als</string>

View file

@ -180,6 +180,7 @@
<string name="common_terms_of_use">Condiciones de uso</string>
<string name="common_today">Hoy</string>
<string name="common_transaction_failed">Transacción fallida</string>
<string name="common_transaction_status">Estado de la transacción</string>
<string name="common_transactions">Transacciones</string>
<string name="common_transfer">Transferencia</string>
<string name="common_understand">Entiendo</string>
@ -187,6 +188,7 @@
<string name="common_unreachable">Inaccesible</string>
<string name="common_unstake">Termine el staking</string>
<string name="common_week">semana</string>
<string name="common_with">con</string>
<string name="common_yes"></string>
<string name="contract_address_copied_message">Dirección del contract copiada</string>
<string name="currency_subtitle_expanded">Redes disponibles</string>
@ -427,6 +429,7 @@
<string name="markets_token_details_fully_diluted_valuation_description">El valor teórico total de una criptomoneda si todas las monedas que podrían existir estuvieran en circulación, incluidas las que actualmente no circulan.</string>
<string name="markets_token_details_fully_diluted_valuation_full">Valoración totalmente diluida</string>
<string name="markets_token_details_genesis_date">Dato de Genesis</string>
<string name="markets_token_details_genesis_date_description">Vacío</string>
<string name="markets_token_details_high">Alto</string>
<string name="markets_token_details_holders">Titulares</string>
<string name="markets_token_details_holders_description">El cambio en el número de poseedores de tokens dentro de un período de tiempo específico</string>
@ -437,6 +440,7 @@
<string name="markets_token_details_liquidity_description">El cambio en la cantidad de liquidez disponible para el token durante el período de tiempo especificado</string>
<string name="markets_token_details_liquidity_full">Liquidez</string>
<string name="markets_token_details_liquidity_index">Índice de liquidez</string>
<string name="markets_token_details_liquidity_index_description">Vacío</string>
<string name="markets_token_details_listed_on">Listado en</string>
<string name="markets_token_details_low">Bajo</string>
<string name="markets_token_details_market_capitalization">Capital. de mercado</string>
@ -453,6 +457,7 @@
<string name="markets_token_details_price_performance">Rendimiento de precios</string>
<string name="markets_token_details_repository">Repositorio</string>
<string name="markets_token_details_security_score">Puntuación de seguridad</string>
<string name="markets_token_details_security_score_description">La puntuación de seguridad de un token es una métrica que evalúa el nivel de seguridad de una blockchain o token en función de varios factores y se genera a partir de las fuentes que se enumeran a continuación.</string>
<string name="markets_token_details_social">Social</string>
<string name="markets_token_details_total_supply">Suministro total</string>
<string name="markets_token_details_total_supply_description">La cantidad máxima de monedas o tokens que pueden existir para una criptomoneda en particular</string>
@ -509,12 +514,12 @@
<string name="onboarding_navbar_title_creating_backup">Creando un backup</string>
<string name="onboarding_seed_button_read_more">Leer más sobre seed phrase</string>
<plurals name="onboarding_seed_generate_message_words_count">
<item quantity="one"></item>
<item quantity="one">Vacío</item>
<item quantity="other">Escriba estas %d palabras en el orden que se indica a continuación y guárdelas en un lugar seguro y secreto.</item>
</plurals>
<string name="onboarding_seed_generate_title">Su seed phrase</string>
<plurals name="onboarding_seed_generate_words_count">
<item quantity="one"></item>
<item quantity="one">Vacío</item>
<item quantity="other">%d palabras</item>
</plurals>
<string name="onboarding_seed_import_message">Para importar su billetera, ingrese su seed phrase en el campo de abajo</string>
@ -566,12 +571,15 @@
<string name="onramp_min_amount_restriction">La cantidad a comprar debe ser como mínimo %s</string>
<string name="onramp_no_available_providers">No hay proveedores disponibles para esta moneda</string>
<string name="onramp_pay_with">Pagar con</string>
<string name="onramp_redirecting_to_provider_subtitle">Podrá completar su transacción en el proveedor externo, %s</string>
<string name="onramp_redirecting_to_provider_title">Redirigiendo a %s...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Nuestros servicios no están disponibles en este país</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Cámbielo o confírmelo</string>
<string name="onramp_residency_bottomsheet_title">Su residencia ha sido identificada como</string>
<string name="onramp_settings_residence">Residencia</string>
<string name="onramp_settings_residence_description">Seleccione el país correcto para garantizar opciones de pago y servicios precisos.</string>
<string name="onramp_settings_title">Ajustes</string>
<string name="onramp_transaction_status_footer_text">Puede comprobar el estado de la transacción desde la página del token</string>
<string name="onramp_via">Vía</string>
<string name="organize_tokens_group">Agrupar</string>
<string name="organize_tokens_sort_by_balance">Por saldo</string>
@ -804,7 +812,7 @@
<string name="staking_unbonding">Desunión</string>
<string name="staking_unlocked_locked">Desbloquear</string>
<string name="staking_unlocking">Desbloqueando</string>
<string name="staking_unstake_amount_requirement_error">La cantidad para el staking debe ser al menos %s</string>
<string name="staking_unstake_amount_requirement_error">La cantidad para el unstaking debe ser al menos %s</string>
<string name="staking_unstake_amount_validation_error">El monto excede el saldo apostado</string>
<string name="staking_unstaked">Sin staking</string>
<string name="staking_unstaking">Unstaking</string>
@ -884,6 +892,7 @@
<string name="transaction_history_transaction_from_address">desde: %s</string>
<string name="transaction_history_transaction_to_address">a: %s</string>
<string name="transaction_history_transaction_validator">validador: %s</string>
<string name="transfer_min_amount_error">Mínimo %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">El monto mínimo para realizar esta transacción es %1$s.</string>
<string name="try_to_load_data_again_button_title">Inténtelo de nuevo</string>
<string name="twin_error_same_card">Ha escaneado la misma tarjeta. Para crear una billetera gemela, necesite escanear la tarjeta con no. %d</string>
@ -949,6 +958,9 @@
<string name="wallet_network_group_title">%s red</string>
<string name="wallet_notification_address_copied">La dirección se copió correctamente</string>
<string name="wallet_notification_no_internet">Sin conexión a internet</string>
<string name="wallet_promo_banner_button_title">Consíguelo ahora con un 10 %% de descuento</string>
<string name="wallet_promo_banner_description">Accede a más de 13 000 criptomonedas. Compra, vende, intercambia y realiza staking con un solo toque.\nVincula hasta tres tarjetas para hacer copias de seguridad.</string>
<string name="wallet_promo_banner_title">Descubre Tangem Wallet</string>
<string name="wallet_settings_title">Ajusta de la wallet</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Use %s o escanee una tarjeta/anillo para desbloquear el acceso a su billetera</string>

View file

@ -884,6 +884,7 @@
<string name="transaction_history_transaction_from_address">de : %s</string>
<string name="transaction_history_transaction_to_address">à : %s</string>
<string name="transaction_history_transaction_validator">validateur : %s</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Le montant minimum pour effectuer cette transaction est %1$s.</string>
<string name="try_to_load_data_again_button_title">Réessayez</string>
<string name="twin_error_same_card">Vous avez scanné la même carte. Pour créer un portefeuille jumeau, vous devez scanner la carte portant le numéro %d</string>

View file

@ -177,6 +177,7 @@
<string name="common_terms_of_use">利用規約</string>
<string name="common_today">今日</string>
<string name="common_transaction_failed">取引が失敗しました</string>
<string name="common_transaction_status">取引状況</string>
<string name="common_transactions">取引</string>
<string name="common_transfer">送金</string>
<string name="common_understand">わかりました</string>
@ -184,6 +185,7 @@
<string name="common_unreachable">アクセスできません</string>
<string name="common_unstake">ステーキング解除</string>
<string name="common_week"></string>
<string name="common_with"></string>
<string name="common_yes">はい</string>
<string name="contract_address_copied_message">コントラクトアドレスをコピーしました!</string>
<string name="currency_subtitle_expanded">利用可能なネットワーク</string>
@ -567,6 +569,7 @@
<string name="onramp_settings_residence">住居</string>
<string name="onramp_settings_residence_description">正確なお支払い方法とサービスを確保するため、正しい国を選択してください。</string>
<string name="onramp_settings_title">設定</string>
<string name="onramp_transaction_status_footer_text">トークンページから取引状況を確認できます</string>
<string name="onramp_via">経由</string>
<string name="organize_tokens_group">グループ</string>
<string name="organize_tokens_sort_by_balance">残高順</string>
@ -943,6 +946,9 @@
<string name="wallet_network_group_title">%sネットワーク</string>
<string name="wallet_notification_address_copied">アドレスがクリップボードにコピーされました</string>
<string name="wallet_notification_no_internet">インターネット接続がありません</string>
<string name="wallet_promo_banner_button_title">今すぐ10 %オフで購入</string>
<string name="wallet_promo_banner_description">1.3万種類以上の暗号資産にアクセス。ワンタップで買付、売却、スワップ、ステーキングが可能です。\nバックアップ用に最大3枚のカードを連携できます。</string>
<string name="wallet_promo_banner_title">Tangemウォレットを見る</string>
<string name="wallet_settings_title">ウォレット設定</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">%sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください</string>

View file

@ -183,6 +183,7 @@
<string name="common_terms_of_use">Условиями использования</string>
<string name="common_today">Сегодня</string>
<string name="common_transaction_failed">Ошибка транзакции</string>
<string name="common_transaction_status">Статус транзакции</string>
<string name="common_transactions">Транзакции</string>
<string name="common_transfer">Перевод</string>
<string name="common_understand">Я понял</string>
@ -190,6 +191,7 @@
<string name="common_unreachable">Недоступно</string>
<string name="common_unstake">Завершить стейкинг</string>
<string name="common_week">неделю</string>
<string name="common_with">с</string>
<string name="common_yes">Да</string>
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
<string name="currency_subtitle_expanded">Доступные сети</string>
@ -576,7 +578,10 @@
<string name="onramp_currency_search">Поиск по валюте</string>
<string name="onramp_max_amount_restriction">Сумма покупки не может быть больше, чем %s</string>
<string name="onramp_min_amount_restriction">Сумма покупки должна составлять минимум %s</string>
<string name="onramp_no_available_providers">Для данной валюты нет доступных провайдеров</string>
<string name="onramp_pay_with">Оплата с</string>
<string name="onramp_redirecting_to_provider_subtitle">Вы сможете завершить транзакцию через сервис стороннего провайдера, %s</string>
<string name="onramp_redirecting_to_provider_title">Переход к %s ...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Наши сервисы недоступны в данной стране</string>
<string name="onramp_settings_title">Настройки</string>
<string name="onramp_via">Через</string>
@ -860,14 +865,14 @@
<string name="toast_balances_shown">Балансы показаны</string>
<string name="toast_undo">Отменить</string>
<string name="token_button_unavailability_generic_description">Выбранная операция в данный момент недоступна. Попробуйте позже.</string>
<string name="token_button_unavailability_reason_buy_unavailable">В данный момент покупка монеты %s недоступна. Следите за нашими обновлениями.</string>
<string name="token_button_unavailability_reason_buy_unavailable">Покупка %s в данный момент не поддерживается ни одним провайдером. Мы работаем над добавлением новых возможностей. Следите за нашими новостями.</string>
<string name="token_button_unavailability_reason_empty_balance_sell">У вас нет средств для продажи. Пополните счет, чтобы иметь возможность продать с него средства.</string>
<string name="token_button_unavailability_reason_empty_balance_send">У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства.</string>
<string name="token_button_unavailability_reason_not_exchangeable">В данный момент обмен монеты %s недоступен. Следите за нашими обновлениями.</string>
<string name="token_button_unavailability_reason_not_exchangeable">Обмен %s в данный момент не поддерживается ни одним провайдером. Мы работаем над добавлением новых возможностей. Следите за нашими новостями.</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">Продажа средств станет доступной после завершения транзакции(-ий) в сети %s</string>
<string name="token_button_unavailability_reason_pending_transaction_send">Отправка средств станет доступной после завершения транзакции(-ий) в сети %s</string>
<string name="token_button_unavailability_reason_sell_unavailable">В данный момент продажа %s недоступна. Следите за нашими обновлениями.</string>
<string name="token_button_unavailability_reason_staking_unavailable">В данный момент стейкинг монеты %s недоступен. Следите за нашими обновлениями.</string>
<string name="token_button_unavailability_reason_sell_unavailable">Продажа %s в данный момент не поддерживается ни одним провайдером. Мы работаем над добавлением новых возможностей. Следите за нашими новостями.</string>
<string name="token_button_unavailability_reason_staking_unavailable">Стейкинг %s в данный момент не поддерживается ни одним провайдером. Мы работаем над добавлением новых возможностей. Следите за нашими новостями.</string>
<string name="token_details_generate_xpub">Сгенерировать XPUB</string>
<string name="token_details_hide_alert_hide">Скрыть</string>
<string name="token_details_hide_alert_message">Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами.</string>
@ -893,6 +898,7 @@
<string name="transaction_history_transaction_from_address">от: %s</string>
<string name="transaction_history_transaction_to_address">на: %s</string>
<string name="transaction_history_transaction_validator">валидатор: %s</string>
<string name="transfer_min_amount_error">Минимум %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Минимальная сумма транзакции равна %1$s.</string>
<string name="try_to_load_data_again_button_title">Попробовать снова</string>
<string name="twin_error_same_card">Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d</string>
@ -958,6 +964,9 @@
<string name="wallet_network_group_title">Сеть %s</string>
<string name="wallet_notification_address_copied">Адрес скопирован в буфер обмена</string>
<string name="wallet_notification_no_internet">Нет соединения с интернетом</string>
<string name="wallet_promo_banner_button_title">Получить с 10%% скидкой</string>
<string name="wallet_promo_banner_description">Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменяйте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. </string>
<string name="wallet_promo_banner_title">Откройте Tangem Wallet </string>
<string name="wallet_settings_title">Настройки кошелька</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Используйте %s или отсканируйте карту/кольцо, чтобы разблокировать доступ к вашему кошельку</string>

View file

@ -186,6 +186,7 @@
<string name="common_terms_of_use">Умовами використання</string>
<string name="common_today">Сьогодні</string>
<string name="common_transaction_failed">Помилка транзакції</string>
<string name="common_transaction_status">Статус транзакції</string>
<string name="common_transactions">Транзакції</string>
<string name="common_transfer">Переказ</string>
<string name="common_understand">Я зрозумів</string>
@ -193,6 +194,7 @@
<string name="common_unreachable">Недоступно</string>
<string name="common_unstake">Скасувати стейкінг</string>
<string name="common_week">тиждень</string>
<string name="common_with">з</string>
<string name="common_yes">Так</string>
<string name="contract_address_copied_message">Адреса контракту скопійована!</string>
<string name="currency_subtitle_expanded">Доступні мережі</string>
@ -465,6 +467,7 @@
<string name="markets_token_details_price_performance">Цінова ефективність</string>
<string name="markets_token_details_repository">Репозиторій</string>
<string name="markets_token_details_security_score">Оцінка безпеки</string>
<string name="markets_token_details_security_score_description">Оцінка безпеки токена це метрика, яка оцінює рівень безпеки блокчейну або токена на основі різних факторів і складається з джерел, перелічених нижче.</string>
<string name="markets_token_details_social">Соцмережі</string>
<string name="markets_token_details_total_supply">Загальна проп.</string>
<string name="markets_token_details_total_supply_description">Максимальна кількість монет або токенів, яка може коли-небудь існувати для певної криптовалюти</string>
@ -521,7 +524,7 @@
<string name="onboarding_navbar_title_creating_backup">Резервна копія</string>
<string name="onboarding_seed_button_read_more">Дізнатися більше про seed-фразу</string>
<plurals name="onboarding_seed_generate_message_words_count">
<item quantity="one">Запишіть це %d слово у порядку, вказаному нижче, та збережіть його у надійному місці.</item>
<item quantity="one"></item>
<item quantity="few">Запишіть ці %d слова у порядку, вказаному нижче, та збережіть їх у надійному місці.</item>
<item quantity="many">Запишіть ці %d слів у порядку, вказаному нижче, та збережіть їх у надійному місці.</item>
<item quantity="other">Запишіть ці %d слів у порядку, вказаному нижче, та збережіть їх у надійному місці.</item>
@ -578,12 +581,18 @@
<string name="onramp_country_search">Пошук за країною</string>
<string name="onramp_country_unavailable">Недоступно</string>
<string name="onramp_currency_search">Пошук по валюті</string>
<string name="onramp_max_amount_restriction">Сума покупки не може бути більше ніж %s</string>
<string name="onramp_min_amount_restriction">Сума покупки повинна бути не менше %s</string>
<string name="onramp_no_available_providers">Для данної валюти немає доступних провайдерів</string>
<string name="onramp_pay_with">Оплата з</string>
<string name="onramp_redirecting_to_provider_subtitle">Ви зможете завершити транзакцію у стороннього постачальника, %s</string>
<string name="onramp_redirecting_to_provider_title">Перенаправлення на %s...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Наші сервіси недоступні в цій країні</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Змінити або підтвердити</string>
<string name="onramp_residency_bottomsheet_title">Ваше місце проживання визначено як</string>
<string name="onramp_settings_residence_description">Будь ласка, виберіть правильну країну, щоб забезпечити точні способи оплати та послуги.</string>
<string name="onramp_settings_title">Параметри</string>
<string name="onramp_transaction_status_footer_text">Ви можете перевірити статус транзакції на сторінці токена</string>
<string name="onramp_via">Через</string>
<string name="organize_tokens_group">Групами</string>
<string name="organize_tokens_sort_by_balance">За балансом</string>
@ -820,6 +829,7 @@
<string name="staking_unbonding">Розблокування</string>
<string name="staking_unlocked_locked">Розблокувати</string>
<string name="staking_unlocking">Розблокування</string>
<string name="staking_unstake_amount_requirement_error">Сума для виведення зі стейкінгу повинна бути не менше %s</string>
<string name="staking_unstake_amount_validation_error">Сума перевищує баланс стейкінгу</string>
<string name="staking_unstaked">Вивід зі стейкінгу</string>
<string name="staking_unstaking">Зняти зі стейкінгу</string>
@ -849,7 +859,7 @@
<string name="swapping_alert_cex_description_with_slippage">У суму входить:\n- комісія провайдера\n- мережева комісія за відправку %1$s з біржі назад на адресу користувача. \n\nПроскакування провайдера становить до %2$s</string>
<string name="swapping_alert_dex_description">Сума включає комісію постачальника послуг.</string>
<string name="swapping_alert_dex_description_with_slippage">Сума включає комісію провайдера послуг. \n\nПроскакування провайдера до %s</string>
<string name="swapping_alert_title">Комісії</string>
<string name="swapping_alert_title">Інформація</string>
<string name="swapping_approve_information_text">Всі децентралізовані біржі вимагають схвалення, щоб запобігти доступу смарт-контрактів до вашого гаманця без вашого дозволу. За задумом смарт-контракти не можуть отримати доступ до ваших токенів без вашого схвалення. \"Розблоковуючи\" свої токени, ви дозволяєте смарт-контракту 1inch витрачати ваші активи. Майнери мережі отримують плату за газ (сплачену вами), щоб зафіксувати цю дію в блокчейні. Ви можете обміняти свій токен після того, як дасте дозвіл.</string>
<string name="swapping_approve_information_title">Підтвердити</string>
<string name="swapping_fee_estimation_error_text">Помилка при розрахунку комісії. Будь ласка, надішліть відгук до служби підтримки.</string>
@ -899,6 +909,7 @@
<string name="transaction_history_transaction_from_address">від: %s</string>
<string name="transaction_history_transaction_to_address">до: %s</string>
<string name="transaction_history_transaction_validator">валідатор: %s</string>
<string name="transfer_min_amount_error">Мінімум %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Мінімальна сума транзакції становить %1$s.</string>
<string name="try_to_load_data_again_button_title">Спробуйте знову</string>
<string name="twin_error_same_card">Ви відсканували одну й ту саму картку. Для створення twin-гаманця вам потрібно відсканувати картку з номером %d</string>

View file

@ -180,6 +180,7 @@
<string name="common_terms_of_use">Terms of Use</string>
<string name="common_today">Today</string>
<string name="common_transaction_failed">Transaction failed</string>
<string name="common_transaction_status">Transaction status</string>
<string name="common_transactions">Transactions</string>
<string name="common_transfer">Transfer</string>
<string name="common_understand">I understand</string>
@ -187,6 +188,7 @@
<string name="common_unreachable">Unreachable</string>
<string name="common_unstake">Unstake</string>
<string name="common_week">week</string>
<string name="common_with">with</string>
<string name="common_yes">Yes</string>
<string name="contract_address_copied_message">Contract address copied!</string>
<string name="currency_subtitle_expanded">Available networks</string>
@ -575,6 +577,7 @@
<string name="onramp_settings_residence">Residence</string>
<string name="onramp_settings_residence_description">Please select the correct country to ensure accurate payment options and services.</string>
<string name="onramp_settings_title">Settings</string>
<string name="onramp_transaction_status_footer_text">You can check the transaction status from the token page</string>
<string name="onramp_via">Via</string>
<string name="organize_tokens_group">Group</string>
<string name="organize_tokens_sort_by_balance">By balance</string>
@ -853,14 +856,14 @@
<string name="toast_balances_shown">Balances shown</string>
<string name="toast_undo">Undo</string>
<string name="token_button_unavailability_generic_description">This operation is currently unavailable. Please try again later.</string>
<string name="token_button_unavailability_reason_buy_unavailable">Buying %s is not available at the moment. Please check our updates.</string>
<string name="token_button_unavailability_reason_buy_unavailable">Buying %s is not supported by current providers, but we are working to add more options.</string>
<string name="token_button_unavailability_reason_empty_balance_sell">You do not have funds to sell. Top up your account to be able to sell funds from it.</string>
<string name="token_button_unavailability_reason_empty_balance_send">You do not have funds to send. Top up your account to be able to send funds from it.</string>
<string name="token_button_unavailability_reason_not_exchangeable">Swapping %s is not available at the moment. Please check our updates.</string>
<string name="token_button_unavailability_reason_not_exchangeable">Swapping %s is not supported by current providers, but we are working to add more options.</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">Selling funds will be available once the pending transaction(s) in network %s is complete</string>
<string name="token_button_unavailability_reason_pending_transaction_send">Sending funds will be available once the pending transaction(s) in network %s is complete</string>
<string name="token_button_unavailability_reason_sell_unavailable">Selling %s is not available at the moment. Please check our updates.</string>
<string name="token_button_unavailability_reason_staking_unavailable">Staking %s is not available at the moment. Please check our updates.</string>
<string name="token_button_unavailability_reason_sell_unavailable">Selling %s is not supported by current providers, but we are working to add more options.</string>
<string name="token_button_unavailability_reason_staking_unavailable">Staking %s is not supported by current providers, but we are working to add more options.</string>
<string name="token_details_generate_xpub">Generate XPUB</string>
<string name="token_details_hide_alert_hide">Hide</string>
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
@ -953,6 +956,9 @@
<string name="wallet_network_group_title">%s network</string>
<string name="wallet_notification_address_copied">Address was copied to clipboard</string>
<string name="wallet_notification_no_internet">No internet connection</string>
<string name="wallet_promo_banner_button_title">Get now with 10%% off</string>
<string name="wallet_promo_banner_description">Access 13,000+ cryptocurrencies. Buy, sell, swap, and stake with a single tap.\nLink up to three cards for a backup.</string>
<string name="wallet_promo_banner_title">Discover Tangem Wallet</string>
<string name="wallet_settings_title">Wallet settings</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Use %s or scan a card/ring to unlock access to your wallet</string>

View file

@ -36,6 +36,7 @@ data class TangemDimens internal constructor(
val radius26: Dp = 26.dp,
val radius28: Dp = 28.dp,
val radius36: Dp = 36.dp,
val radius40: Dp = 40.dp,
// endregion Radius
// region Size
val size0: Dp = 0.dp,

View file

@ -1,9 +1,11 @@
package com.tangem.data.markets.converters
import android.net.Uri
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.isSupportedInApp
import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse
import com.tangem.domain.markets.BuildConfig
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenQuotes
import com.tangem.utils.converter.Converter
@ -11,6 +13,9 @@ import java.math.BigDecimal
internal object TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, TokenMarketInfo> {
private const val PROD_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/security_provider/"
private const val DEV_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api.dev/security_provider/"
override fun convert(value: TokenMarketInfoResponse): TokenMarketInfo {
return with(value) {
TokenMarketInfo(
@ -23,6 +28,7 @@ internal object TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, To
fullDescription = fullDescription,
insights = insights?.convert(),
metrics = metrics?.convert(),
securityData = securityData?.convert(),
links = links?.convert(),
pricePerformance = pricePerformance?.convert(),
exchangesAmount = exchangesAmount,
@ -105,6 +111,31 @@ internal object TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, To
)
}
private fun TokenMarketInfoResponse.SecurityData.convert(): TokenMarketInfo.SecurityData {
return TokenMarketInfo.SecurityData(
totalSecurityScore = totalSecurityScore,
providerData = providerData.map { it.convert() },
)
}
private fun TokenMarketInfoResponse.ProviderData.convert(): TokenMarketInfo.ProviderData {
return TokenMarketInfo.ProviderData(
providerId = providerId,
providerName = providerName,
urlData = link?.convertToUrlData(),
securityScore = securityScore,
lastAuditDate = lastAuditDate,
iconUrl = "${getImageHost()}large/$providerId.png",
)
}
private fun String.convertToUrlData(): TokenMarketInfo.ProviderUrlData {
return TokenMarketInfo.ProviderUrlData(
fullUrl = this,
rootHost = this.extractMainDomainOrNull(),
)
}
private fun TokenMarketInfoResponse.Links.convert(): TokenMarketInfo.Links {
return TokenMarketInfo.Links(
officialLinks = officialLinks?.convert(),
@ -139,4 +170,27 @@ internal object TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, To
high = high,
)
}
private fun String.extractMainDomainOrNull(): String? {
return runCatching {
val uri = Uri.parse(this)
val host = uri.host ?: return null
val parts = host.split(".")
if (parts.size >= 2) {
parts.takeLast(2).joinToString(".")
} else {
host
}
}.getOrNull()
}
private fun getImageHost(): String {
return if (BuildConfig.TESTER_MENU_ENABLED) {
DEV_IMAGE_HOST
} else {
PROD_IMAGE_HOST
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.markets
import org.joda.time.DateTime
import java.math.BigDecimal
data class TokenMarketInfo(
@ -12,6 +13,7 @@ data class TokenMarketInfo(
val fullDescription: String?,
val insights: Insights?,
val metrics: Metrics?,
val securityData: SecurityData?,
val links: Links?,
val pricePerformance: PricePerformance?,
val exchangesAmount: Int?,
@ -51,6 +53,25 @@ data class TokenMarketInfo(
val fullyDilutedValuation: BigDecimal?,
)
data class SecurityData(
val totalSecurityScore: Float,
val providerData: List<ProviderData>,
)
data class ProviderData(
val providerId: String,
val providerName: String,
val urlData: ProviderUrlData?,
val iconUrl: String,
val securityScore: Float,
val lastAuditDate: DateTime?,
)
data class ProviderUrlData(
val fullUrl: String,
val rootHost: String?,
)
data class Links(
val officialLinks: List<Link>?,
val social: List<Link>?,

View file

@ -88,6 +88,9 @@ internal class MarketsTokenDetailsModel @Inject constructor(
// === Analytics ===
analyticsEventHandler.send(analyticsEventBuilder.linkClicked(linkTitle = link.title))
},
onSecurityScoreProviderLinkClick = {
urlOpener.openUrl(it)
},
// === Analytics ===
onPricePerformanceIntervalChanged = {
analyticsEventHandler.send(

View file

@ -1,35 +1,57 @@
package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.model.formatter.MarketsDateTimeFormatters
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM
import com.tangem.features.markets.impl.R
import com.tangem.utils.converter.Converter
// TODO implement when backend is ready
@Stable
internal class SecurityScoreConverter(
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
) : Converter<Unit, SecurityScoreUM> {
private val onInfoClick: (SecurityScoreBottomSheetContent) -> Unit,
private val onSecurityScoreProviderLinkClick: (String) -> Unit,
) : Converter<TokenMarketInfo.SecurityData, SecurityScoreUM> {
override fun convert(value: Unit): SecurityScoreUM {
return with(value) {
SecurityScoreUM(
score = 4.7f,
description = "Based on 3 ratings",
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_security_score),
body = stringReference("markets_token_details_security_score_description"),
// FIXME
// resourceReference(R.string.markets_token_details_security_score_description)
),
)
},
)
}
override fun convert(value: TokenMarketInfo.SecurityData): SecurityScoreUM {
val ratingsCount = value.providerData.size
return SecurityScoreUM(
score = value.totalSecurityScore,
description = pluralReference(
id = R.plurals.markets_token_details_based_on_ratings,
count = ratingsCount,
formatArgs = wrappedList(ratingsCount),
),
onInfoClick = {
onInfoClick(
SecurityScoreBottomSheetContent(
title = resourceReference(R.string.markets_token_details_security_score),
description = resourceReference(R.string.markets_token_details_security_score_description),
providers = value.providerData.map {
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
name = it.providerName,
lastAuditDate = it.lastAuditDate?.let { date ->
MarketsDateTimeFormatters.formatAsDate(date.millis)
},
score = it.securityScore,
urlData = it.urlData?.let {
urlData ->
SecurityScoreBottomSheetContent.SecurityScoreProviderUrlData(
fullUrl = urlData.fullUrl,
rootHost = urlData.rootHost,
)
},
iconUrl = it.iconUrl,
)
},
onProviderLinkClick = onSecurityScoreProviderLinkClick,
),
)
},
)
}
}

View file

@ -1,10 +1,10 @@
package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.LinksUM
import com.tangem.features.markets.details.impl.ui.state.ListedOnUM
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
@ -12,11 +12,13 @@ import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
@Stable
@Suppress("LongParameterList")
internal class TokenMarketInfoConverter(
private val appCurrency: Provider<AppCurrency>,
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
private val onInfoClick: (TangemBottomSheetConfigContent) -> Unit,
private val onListedOnClick: (Int) -> Unit,
onLinkClick: (LinksUM.Link) -> Unit,
onSecurityScoreProviderLinkClick: (String) -> Unit,
onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit,
onInsightsIntervalChanged: (PriceChangeInterval) -> Unit,
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.InformationBlocks> {
@ -27,9 +29,10 @@ internal class TokenMarketInfoConverter(
onIntervalChanged = onInsightsIntervalChanged,
)
@Suppress("UnusedPrivateMember")
// TODO second markets iteration
private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick)
private val securityScoreConverter = SecurityScoreConverter(
onInfoClick = onInfoClick,
onSecurityScoreProviderLinkClick = onSecurityScoreProviderLinkClick,
)
private val pricePerformanceConverter = PricePerformanceConverter(
appCurrency = appCurrency,
onIntervalChanged = onPricePerformanceIntervalChanged,
@ -46,7 +49,7 @@ internal class TokenMarketInfoConverter(
val exchangesAmount = value.exchangesAmount
return MarketsTokenDetailsUM.InformationBlocks(
insights = value.insights?.let { insightsConverter.convert(it) },
securityScore = null,
securityScore = value.securityData?.let { securityScoreConverter.convert(it) },
metrics = value.metrics?.let { metricsConverter.convert(it) },
pricePerformance = value.pricePerformance?.let {
pricePerformanceConverter.convert(

View file

@ -55,7 +55,7 @@ internal object MarketsDateTimeFormatters {
R.string.common_range_with_space,
wrappedList(
stringReference(
startTimestamp.formatAsDateTime(MarketsDateTimeFormatters.dateTimeMMMFormatter),
startTimestamp.formatAsDateTime(dateTimeMMMFormatter),
),
resourceReference(R.string.common_now),
),
@ -68,7 +68,7 @@ internal object MarketsDateTimeFormatters {
R.string.common_range_with_space,
wrappedList(
stringReference(
startTimestamp.formatAsDateTime(MarketsDateTimeFormatters.dateFormatter),
startTimestamp.formatAsDateTime(dateFormatter),
),
resourceReference(R.string.common_now),
),
@ -89,7 +89,7 @@ internal object MarketsDateTimeFormatters {
R.string.common_range_with_space,
wrappedList(
stringReference(
markerTimestamp.toLong().formatAsDateTime(MarketsDateTimeFormatters.dateTimeMMMFormatter),
markerTimestamp.toLong().formatAsDateTime(dateTimeMMMFormatter),
),
resourceReference(R.string.common_now),
),
@ -103,7 +103,7 @@ internal object MarketsDateTimeFormatters {
R.string.common_range_with_space,
wrappedList(
stringReference(
markerTimestamp.toLong().formatAsDateTime(MarketsDateTimeFormatters.dateFormatter),
markerTimestamp.toLong().formatAsDateTime(dateFormatter),
),
resourceReference(R.string.common_now),
),
@ -128,10 +128,14 @@ internal object MarketsDateTimeFormatters {
fun getDefaultDateTimeString(interval: PriceChangeInterval, currentTimestamp: Long): TextReference {
return formatDateByInterval(
interval = interval,
startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval(
startTimestamp = getStartTimestampByInterval(
interval = interval,
currentTimestamp = currentTimestamp,
),
)
}
fun formatAsDate(timestamp: Long): String {
return timestamp.formatAsDateTime(dateFormatter)
}
}

View file

@ -37,6 +37,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.features.markets.details.impl.ui.components.*
import com.tangem.features.markets.details.impl.ui.components.ExchangesBottomSheet
import com.tangem.features.markets.details.impl.ui.components.InfoBottomSheet
import com.tangem.features.markets.details.impl.ui.components.MarketTokenDetailsChart
@ -44,6 +45,7 @@ import com.tangem.features.markets.details.impl.ui.components.tokenMarketDetails
import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent
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.details.impl.ui.state.SecurityScoreBottomSheetContent
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.persistentListOf
@ -72,6 +74,7 @@ internal fun MarketsTokenDetailsContent(
when (state.bottomSheetConfig.content) {
is InfoBottomSheetContent -> InfoBottomSheet(config = state.bottomSheetConfig)
is SecurityScoreBottomSheetContent -> SecurityScoreBottomSheet(config = state.bottomSheetConfig)
is ExchangesBottomSheetContent -> ExchangesBottomSheet(config = state.bottomSheetConfig)
}
}

View file

@ -0,0 +1,96 @@
package com.tangem.features.markets.details.impl.ui.components
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.impl.R
import kotlin.math.round
private const val STARS_COUNT = 5
@Composable
internal fun ScoreStarsBlock(
score: Float,
horizontalSpacing: Dp,
scoreTextStyle: TextStyle,
modifier: Modifier = Modifier,
) {
val rounded = score.roundTo1decimal()
val percentage = rounded / STARS_COUNT
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(horizontalSpacing),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = rounded.toString(),
style = scoreTextStyle,
color = TangemTheme.colors.text.primary1,
)
Stars(fraction = percentage)
}
}
@Suppress("MagicNumber")
@Composable
private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
val grayColor = TangemTheme.colors.icon.inactive
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
verticalAlignment = Alignment.CenterVertically,
) {
repeat(times = 5) { i ->
Box(
modifier = Modifier.size(TangemTheme.dimens.size16),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier
.requiredSize(16.dp)
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithCache {
onDrawWithContent {
val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0)
val starFractionFloat = starFraction
.toFloat()
.roundTo1decimal()
drawContent()
drawRect(
color = grayColor,
topLeft = Offset(x = size.width * starFractionFloat, y = 0f),
size = Size(size.width * (1 - starFractionFloat), size.height),
blendMode = BlendMode.SrcIn,
)
}
},
imageVector = ImageVector.vectorResource(R.drawable.ic_star_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
)
}
}
}
}
@Suppress("MagicNumber")
private fun Float.roundTo1decimal(): Float {
return round(this * 10) / 10
}

View file

@ -1,42 +1,28 @@
package com.tangem.features.markets.details.impl.ui.components
import android.content.res.Configuration
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.text.TooltipText
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM
import com.tangem.features.markets.impl.R
import kotlin.math.round
private const val STARS_COUNT = 5
@Composable
internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) {
val rounded = state.score.roundTo1decimal()
val percentage = rounded / STARS_COUNT
InformationBlock(
modifier = modifier,
title = {
@ -51,7 +37,7 @@ internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Mod
)
Text(
text = state.description,
text = state.description.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
@ -60,72 +46,18 @@ internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Mod
}
},
action = {
Row(
ScoreStarsBlock(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = rounded.toString(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
Stars(fraction = percentage)
}
score = state.score,
scoreTextStyle = TangemTheme.typography.body1,
horizontalSpacing = TangemTheme.dimens.spacing8,
)
},
)
}
@Suppress("MagicNumber")
@Composable
private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) {
val grayColor = TangemTheme.colors.icon.inactive
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
verticalAlignment = Alignment.CenterVertically,
) {
repeat(times = 5) { i ->
Box(
modifier = Modifier.size(TangemTheme.dimens.size16),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier
.requiredSize(13.dp)
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithCache {
onDrawWithContent {
val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0)
val starFractionFloat = starFraction
.toFloat()
.roundTo1decimal()
drawContent()
drawRect(
color = grayColor,
topLeft = Offset(x = size.width * starFractionFloat, y = 0f),
size = Size(size.width * (1 - starFractionFloat), size.height),
blendMode = BlendMode.SrcIn,
)
}
},
imageVector = ImageVector.vectorResource(R.drawable.ic_star_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
)
}
}
}
}
@Suppress("MagicNumber")
private fun Float.roundTo1decimal(): Float {
return round(this * 10) / 10
}
@Composable
internal fun SecurityScorePlaceHolder(modifier: Modifier = Modifier) {
private fun SecurityScorePlaceHolder(modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
title = {
@ -177,7 +109,7 @@ private fun ContentPreview() {
SecurityScoreBlock(
state = SecurityScoreUM(
score = 3.5f,
description = "Based on 3 ratings",
description = stringReference("Based on 3 ratings"),
onInfoClick = {},
),
)

View file

@ -0,0 +1,191 @@
package com.tangem.features.markets.details.impl.ui.components
import android.content.res.Configuration
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material3.ripple
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.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEachIndexed
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.*
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.inputrow.inner.DividerContainer
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.details.impl.ui.preview.SecurityScorePreviewData
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent
@Composable
internal fun SecurityScoreBottomSheet(config: TangemBottomSheetConfig) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
TangemBottomSheet<SecurityScoreBottomSheetContent>(
config = config,
containerColor = TangemTheme.colors.background.secondary,
addBottomInsets = false,
title = { TangemBottomSheetTitle(title = it.title) },
content = { content ->
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
Text(
text = content.description.resolveReference(),
style = TangemTheme.typography.body2.copy(
color = TangemTheme.colors.text.secondary,
),
)
SpacerH12()
content.providers.fastForEachIndexed { index, provider ->
DividerContainer(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = content.providers.lastIndex,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action),
showDivider = index != content.providers.lastIndex,
) {
SecurityScoreProviderRow(
providerUM = provider,
onLinkClick = content.onProviderLinkClick,
)
}
}
SpacerH16()
SpacerH(bottomBarHeight)
}
},
)
}
@Composable
private fun SecurityScoreProviderRow(
providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM,
onLinkClick: (String) -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing12)
.heightIn(min = TangemTheme.dimens.size68),
verticalAlignment = Alignment.CenterVertically,
) {
SubcomposeAsyncImage(
modifier = Modifier
.size(size = TangemTheme.dimens.size40)
.clip(TangemTheme.shapes.roundedCorners8),
model = ImageRequest.Builder(context = LocalContext.current)
.data(providerUM.iconUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) },
error = { RectangleShimmer(radius = TangemTheme.dimens.radius8) },
contentDescription = null,
)
Column(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing12),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
Text(
text = providerUM.name,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
providerUM.lastAuditDate?.let {
Text(
text = providerUM.lastAuditDate,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
SpacerWMax()
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
modifier = Modifier.clickable(
enabled = providerUM.urlData != null,
indication = ripple(bounded = false),
interactionSource = remember { MutableInteractionSource() },
onClick = { providerUM.urlData?.let { onLinkClick(it.fullUrl) } },
),
) {
ScoreStarsBlock(
score = providerUM.score,
scoreTextStyle = TangemTheme.typography.body2,
horizontalSpacing = TangemTheme.dimens.spacing3,
)
UrlBlock(providerUM)
}
}
}
@Composable
private fun UrlBlock(providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM) {
val urlData = providerUM.urlData
val rootHost = urlData?.rootHost
if (urlData != null && rootHost != null) {
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
Text(
text = urlData.rootHost,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
Icon(
modifier = Modifier
.size(TangemTheme.dimens.size16),
painter = painterResource(id = R.drawable.ic_arrow_top_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SecurityScoreBottomSheetPreview() {
TangemThemePreview {
SecurityScoreBottomSheet(
config = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = {},
content = SecurityScorePreviewData.bottomSheetContent,
),
)
}
}

View file

@ -0,0 +1,60 @@
package com.tangem.features.markets.details.impl.ui.preview
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent
internal object SecurityScorePreviewData {
val bottomSheetContent = SecurityScoreBottomSheetContent(
title = stringReference("Security score"),
description = stringReference(
"Security score of a token is a metric that assesses the " +
"security level of a blockchain or token based on various factors and is compiled from " +
"the sources listed below.",
),
providers = listOf(
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
name = "Moralis",
lastAuditDate = "21.10.2024",
score = 4.9F,
urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUrlData(
fullUrl = "https://moralis.com/",
rootHost = "moralis.com",
),
iconUrl = "",
),
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
name = "Certik",
lastAuditDate = "10.07.2024",
score = 4.6F,
urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUrlData(
fullUrl = "https://certik.com/",
rootHost = "certik.com",
),
iconUrl = "",
),
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
name = "Cyberscope",
lastAuditDate = "25.06.2023",
score = 4.5F,
urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUrlData(
fullUrl = "https://cyberscope.com/",
rootHost = "cyberscope.com",
),
iconUrl = "",
),
SecurityScoreBottomSheetContent.SecurityScoreProviderUM(
name = "TokenInsight",
lastAuditDate = "17.01.2022",
score = 4.0F,
urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUrlData(
fullUrl = "https://tokeninsight.com/",
rootHost = "tokeninsight.com",
),
iconUrl = "",
),
),
onProviderLinkClick = {},
)
}

View file

@ -0,0 +1,25 @@
package com.tangem.features.markets.details.impl.ui.state
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
internal data class SecurityScoreBottomSheetContent(
val title: TextReference,
val description: TextReference,
val providers: List<SecurityScoreProviderUM>,
val onProviderLinkClick: (String) -> Unit,
) : TangemBottomSheetConfigContent {
data class SecurityScoreProviderUM(
val name: String,
val lastAuditDate: String?,
val score: Float,
val urlData: SecurityScoreProviderUrlData?,
val iconUrl: String?,
)
data class SecurityScoreProviderUrlData(
val fullUrl: String,
val rootHost: String?,
)
}

View file

@ -1,9 +1,10 @@
package com.tangem.features.markets.details.impl.ui.state
import androidx.annotation.FloatRange
import com.tangem.core.ui.extensions.TextReference
internal data class SecurityScoreUM(
@FloatRange(from = 0.0, to = 5.0) val score: Float,
val description: String,
val description: TextReference,
val onInfoClick: () -> Unit,
)