From 546bcc908f9d0b2a5c1981b447632bdc6e854aae Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Nov 2024 10:44:43 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../api/common/adapter/DateTimeAdapter.kt | 9 +- .../api/common/adapter/LocalDateAdapter.kt | 8 +- .../response/TokenMarketInfoResponse.kt | 25 +++ core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-es/strings.xml | 18 +- core/res/src/main/res/values-fr/strings.xml | 1 + core/res/src/main/res/values-ja/strings.xml | 6 + core/res/src/main/res/values-ru/strings.xml | 17 +- .../src/main/res/values-uk-rUA/strings.xml | 15 +- core/res/src/main/res/values/strings.xml | 14 +- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../converters/TokenMarketInfoConverter.kt | 54 +++++ .../tangem/domain/markets/TokenMarketInfo.kt | 21 ++ .../impl/model/MarketsTokenDetailsModel.kt | 3 + .../converters/SecurityScoreConverter.kt | 66 ++++-- .../converters/TokenMarketInfoConverter.kt | 15 +- .../formatter/MarketsDateTimeFormatters.kt | 14 +- .../impl/ui/MarketsTokenDetailsContent.kt | 3 + .../impl/ui/components/ScoreStarsBlock.kt | 96 +++++++++ .../impl/ui/components/SecurityScoreBlock.kt | 88 +------- .../ui/components/SecurityScoreBottomSheet.kt | 191 ++++++++++++++++++ .../ui/preview/SecurityScorePreviewData.kt | 60 ++++++ .../state/SecurityScoreBottomSheetContent.kt | 25 +++ .../details/impl/ui/state/SecurityScoreUM.kt | 3 +- 24 files changed, 624 insertions(+), 131 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt index 5e637ed8c8..4149bf22f8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt @@ -7,9 +7,12 @@ internal class DateTimeAdapter : JsonAdapter() { @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() + } else { + val dateString = reader.nextString() + DateTime.parse(dateString) + } } @ToJson diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt index 6a7cd420c6..d87504268b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt @@ -10,8 +10,12 @@ internal class LocalDateAdapter : JsonAdapter() { @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() + } else { + val dateString = reader.nextString() + return LocalDate.parse(dateString, formatter) + } } @ToJson diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index 64b168072f..e18e222fd9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -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, + ) + + @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") diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 5e53fcf4cc..65a4990969 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -568,7 +568,7 @@ Keine verfügbaren Anbieter für diese Währung Bezahlen mit Du kannst Deine Transaktion beim Drittanbieter %s abschließen. - Umleitung auf %s... + Umleitung auf %s … Unsere Dienstleistungen sind in diesem Land nicht verfügbar Änder oder bestätige bitte Dein Wohnsitz wurde identifiziert als diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index f8405d31f3..ee04d97a2f 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -180,6 +180,7 @@ Condiciones de uso Hoy Transacción fallida + Estado de la transacción Transacciones Transferencia Entiendo @@ -187,6 +188,7 @@ Inaccesible Termine el staking semana + con Dirección del contract copiada Redes disponibles @@ -427,6 +429,7 @@ 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. Valoración totalmente diluida Dato de Genesis + Vacío Alto Titulares El cambio en el número de poseedores de tokens dentro de un período de tiempo específico @@ -437,6 +440,7 @@ El cambio en la cantidad de liquidez disponible para el token durante el período de tiempo especificado Liquidez Índice de liquidez + Vacío Listado en Bajo Capital. de mercado @@ -453,6 +457,7 @@ Rendimiento de precios Repositorio Puntuación de seguridad + 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. Social Suministro total La cantidad máxima de monedas o tokens que pueden existir para una criptomoneda en particular @@ -509,12 +514,12 @@ Creando un backup Leer más sobre seed phrase - + Vacío Escriba estas %d palabras en el orden que se indica a continuación y guárdelas en un lugar seguro y secreto. Su seed phrase - + Vacío %d palabras Para importar su billetera, ingrese su seed phrase en el campo de abajo @@ -566,12 +571,15 @@ La cantidad a comprar debe ser como mínimo %s No hay proveedores disponibles para esta moneda Pagar con + Podrá completar su transacción en el proveedor externo, %s + Redirigiendo a %s... Nuestros servicios no están disponibles en este país Cámbielo o confírmelo Su residencia ha sido identificada como Residencia Seleccione el país correcto para garantizar opciones de pago y servicios precisos. Ajustes + Puede comprobar el estado de la transacción desde la página del token Vía Agrupar Por saldo @@ -804,7 +812,7 @@ Desunión Desbloquear Desbloqueando - La cantidad para el staking debe ser al menos %s + La cantidad para el unstaking debe ser al menos %s El monto excede el saldo apostado Sin staking Unstaking @@ -884,6 +892,7 @@ desde: %s a: %s validador: %s + Mínimo %s El monto mínimo para realizar esta transacción es %1$s. Inténtelo de nuevo Ha escaneado la misma tarjeta. Para crear una billetera gemela, necesite escanear la tarjeta con no. %d @@ -949,6 +958,9 @@ %s red La dirección se copió correctamente Sin conexión a internet + Consíguelo ahora con un 10 %% de descuento + 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. + Descubre Tangem Wallet Ajusta de la wallet Tangem Use %s o escanee una tarjeta/anillo para desbloquear el acceso a su billetera diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index dba4e36c8b..c9983efdb7 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -884,6 +884,7 @@ de : %s à : %s validateur : %s + Minimum %s Le montant minimum pour effectuer cette transaction est %1$s. Réessayez Vous avez scanné la même carte. Pour créer un portefeuille jumeau, vous devez scanner la carte portant le numéro %d diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index af02575293..3d8d8d15d5 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -177,6 +177,7 @@ 利用規約 今日 取引が失敗しました + 取引状況 取引 送金 わかりました @@ -184,6 +185,7 @@ アクセスできません ステーキング解除 + はい コントラクトアドレスをコピーしました! 利用可能なネットワーク @@ -567,6 +569,7 @@ 住居 正確なお支払い方法とサービスを確保するため、正しい国を選択してください。 設定 + トークンページから取引状況を確認できます 経由 グループ 残高順 @@ -943,6 +946,9 @@ %sネットワーク アドレスがクリップボードにコピーされました インターネット接続がありません + 今すぐ10 %オフで購入 + 1.3万種類以上の暗号資産にアクセス。ワンタップで買付、売却、スワップ、ステーキングが可能です。\nバックアップ用に最大3枚のカードを連携できます。 + Tangemウォレットを見る ウォレット設定 Tangem %sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 952af02796..9a43af9607 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -183,6 +183,7 @@ Условиями использования Сегодня Ошибка транзакции + Статус транзакции Транзакции Перевод Я понял @@ -190,6 +191,7 @@ Недоступно Завершить стейкинг неделю + с Да Адрес контракта скопирован! Доступные сети @@ -576,7 +578,10 @@ Поиск по валюте Сумма покупки не может быть больше, чем %s Сумма покупки должна составлять минимум %s + Для данной валюты нет доступных провайдеров Оплата с + Вы сможете завершить транзакцию через сервис стороннего провайдера, %s + Переход к %s ... Наши сервисы недоступны в данной стране Настройки Через @@ -860,14 +865,14 @@ Балансы показаны Отменить Выбранная операция в данный момент недоступна. Попробуйте позже. - В данный момент покупка монеты %s недоступна. Следите за нашими обновлениями. + Покупка %s в данный момент не поддерживается ни одним провайдером. Мы работаем над добавлением новых возможностей. Следите за нашими новостями. У вас нет средств для продажи. Пополните счет, чтобы иметь возможность продать с него средства. У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. - В данный момент обмен монеты %s недоступен. Следите за нашими обновлениями. + Обмен %s в данный момент не поддерживается ни одним провайдером. Мы работаем над добавлением новых возможностей. Следите за нашими новостями. Продажа средств станет доступной после завершения транзакции(-ий) в сети %s Отправка средств станет доступной после завершения транзакции(-ий) в сети %s - В данный момент продажа %s недоступна. Следите за нашими обновлениями. - В данный момент стейкинг монеты %s недоступен. Следите за нашими обновлениями. + Продажа %s в данный момент не поддерживается ни одним провайдером. Мы работаем над добавлением новых возможностей. Следите за нашими новостями. + Стейкинг %s в данный момент не поддерживается ни одним провайдером. Мы работаем над добавлением новых возможностей. Следите за нашими новостями. Сгенерировать XPUB Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. @@ -893,6 +898,7 @@ от: %s на: %s валидатор: %s + Минимум %s Минимальная сумма транзакции равна %1$s. Попробовать снова Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d @@ -958,6 +964,9 @@ Сеть %s Адрес скопирован в буфер обмена Нет соединения с интернетом + Получить с 10%% скидкой + Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменяйте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. + Откройте Tangem Wallet Настройки кошелька Tangem Используйте %s или отсканируйте карту/кольцо, чтобы разблокировать доступ к вашему кошельку diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index c3f58becbb..8f07d981bf 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -186,6 +186,7 @@ Умовами використання Сьогодні Помилка транзакції + Статус транзакції Транзакції Переказ Я зрозумів @@ -193,6 +194,7 @@ Недоступно Скасувати стейкінг тиждень + з Так Адреса контракту скопійована! Доступні мережі @@ -465,6 +467,7 @@ Цінова ефективність Репозиторій Оцінка безпеки + Оцінка безпеки токена – це метрика, яка оцінює рівень безпеки блокчейну або токена на основі різних факторів і складається з джерел, перелічених нижче. Соцмережі Загальна проп. Максимальна кількість монет або токенів, яка може коли-небудь існувати для певної криптовалюти @@ -521,7 +524,7 @@ Резервна копія Дізнатися більше про seed-фразу - Запишіть це %d слово у порядку, вказаному нижче, та збережіть його у надійному місці. + Запишіть ці %d слова у порядку, вказаному нижче, та збережіть їх у надійному місці. Запишіть ці %d слів у порядку, вказаному нижче, та збережіть їх у надійному місці. Запишіть ці %d слів у порядку, вказаному нижче, та збережіть їх у надійному місці. @@ -578,12 +581,18 @@ Пошук за країною Недоступно Пошук по валюті + Сума покупки не може бути більше ніж %s + Сума покупки повинна бути не менше %s + Для данної валюти немає доступних провайдерів Оплата з + Ви зможете завершити транзакцію у стороннього постачальника, %s + Перенаправлення на %s... Наші сервіси недоступні в цій країні Змінити або підтвердити Ваше місце проживання визначено як Будь ласка, виберіть правильну країну, щоб забезпечити точні способи оплати та послуги. Параметри + Ви можете перевірити статус транзакції на сторінці токена Через Групами За балансом @@ -820,6 +829,7 @@ Розблокування Розблокувати Розблокування + Сума для виведення зі стейкінгу повинна бути не менше %s Сума перевищує баланс стейкінгу Вивід зі стейкінгу Зняти зі стейкінгу @@ -849,7 +859,7 @@ У суму входить:\n- комісія провайдера\n- мережева комісія за відправку %1$s з біржі назад на адресу користувача. \n\nПроскакування провайдера становить до %2$s Сума включає комісію постачальника послуг. Сума включає комісію провайдера послуг. \n\nПроскакування провайдера до %s - Комісії + Інформація Всі децентралізовані біржі вимагають схвалення, щоб запобігти доступу смарт-контрактів до вашого гаманця без вашого дозволу. За задумом смарт-контракти не можуть отримати доступ до ваших токенів без вашого схвалення. \"Розблоковуючи\" свої токени, ви дозволяєте смарт-контракту 1inch витрачати ваші активи. Майнери мережі отримують плату за газ (сплачену вами), щоб зафіксувати цю дію в блокчейні. Ви можете обміняти свій токен після того, як дасте дозвіл. Підтвердити Помилка при розрахунку комісії. Будь ласка, надішліть відгук до служби підтримки. @@ -899,6 +909,7 @@ від: %s до: %s валідатор: %s + Мінімум %s Мінімальна сума транзакції становить %1$s. Спробуйте знову Ви відсканували одну й ту саму картку. Для створення twin-гаманця вам потрібно відсканувати картку з номером %d diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 2592e85ed6..6978552ae0 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -180,6 +180,7 @@ Terms of Use Today Transaction failed + Transaction status Transactions Transfer I understand @@ -187,6 +188,7 @@ Unreachable Unstake week + with Yes Contract address copied! Available networks @@ -575,6 +577,7 @@ Residence Please select the correct country to ensure accurate payment options and services. Settings + You can check the transaction status from the token page Via Group By balance @@ -853,14 +856,14 @@ Balances shown Undo This operation is currently unavailable. Please try again later. - Buying %s is not available at the moment. Please check our updates. + Buying %s is not supported by current providers, but we are working to add more options. You do not have funds to sell. Top up your account to be able to sell funds from it. You do not have funds to send. Top up your account to be able to send funds from it. - Swapping %s is not available at the moment. Please check our updates. + Swapping %s is not supported by current providers, but we are working to add more options. Selling funds will be available once the pending transaction(s) in network %s is complete Sending funds will be available once the pending transaction(s) in network %s is complete - Selling %s is not available at the moment. Please check our updates. - Staking %s is not available at the moment. Please check our updates. + Selling %s is not supported by current providers, but we are working to add more options. + Staking %s is not supported by current providers, but we are working to add more options. Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. @@ -953,6 +956,9 @@ %s network Address was copied to clipboard No internet connection + Get now with 10%% off + Access 13,000+ cryptocurrencies. Buy, sell, swap, and stake with a single tap.\nLink up to three cards for a backup. + Discover Tangem Wallet Wallet settings Tangem Use %s or scan a card/ring to unlock access to your wallet diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 9617ee7825..7698a8067d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -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, diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt index 7df1e1a203..7910c58025 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -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 { + 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= 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 + } + } } \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt index f8f62337a0..c53274d7a3 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt @@ -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, + ) + + 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?, val social: List?, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 9d7e300646..aa748cfe5f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -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( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt index 302aeb7258..906842ebf0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt @@ -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 { + private val onInfoClick: (SecurityScoreBottomSheetContent) -> Unit, + private val onSecurityScoreProviderLinkClick: (String) -> Unit, +) : Converter { - override fun convert(value: Unit): SecurityScoreUM { - return with(value) { - SecurityScoreUM( - score = 4.7f, - description = "Based on 3 ratings", - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_security_score), - body = stringReference("markets_token_details_security_score_description"), - // FIXME - // resourceReference(R.string.markets_token_details_security_score_description) - ), - ) - }, - ) - } + 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, + ), + ) + }, + ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt index 3ab8df1094..d857983a3a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt @@ -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, - 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 { @@ -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( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt index 5abc296917..46ba611519 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt @@ -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) + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index 4e46c48655..59c5d3d8f1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -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) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt new file mode 100644 index 0000000000..d7d047c38b --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt @@ -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 +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt index 66a75660ef..db8023992c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt @@ -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 = {}, ), ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt new file mode 100644 index 0000000000..69cf38aa34 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt @@ -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( + 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, + ), + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt new file mode 100644 index 0000000000..45e80ad9b7 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt @@ -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 = {}, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt new file mode 100644 index 0000000000..bfac7fcab1 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt @@ -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, + 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?, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt index 35f35d4b9c..d4ebd3a9ee 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt @@ -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, ) \ No newline at end of file