Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-20 06:42:59 +03:00
commit 54216a69e5
59 changed files with 414 additions and 246 deletions

View file

@ -18,10 +18,14 @@ class AmplitudeAnalyticsHandler(
}
class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
!data.isDebug -> AmplitudeClient(data.application, data.config.amplitudeApiKey)
data.isDebug && data.logConfig.amplitude -> AmplitudeLogClient(data.jsonConverter)
else -> null
}?.let { AmplitudeAnalyticsHandler(it) }
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler {
return AmplitudeAnalyticsHandler(
client = if (data.logConfig.amplitude) {
AmplitudeLogClient(data.jsonConverter)
} else {
AmplitudeClient(data.application, data.config.amplitudeApiKey)
},
)
}
}
}

View file

@ -239,9 +239,8 @@ class WalletConnectSdkHelper {
private suspend fun signTransaction(data: WcTransactionData, cardId: String?): String? {
val dataToSign = EthereumUtils.buildTransactionToSign(
transactionData = data.transaction,
nonce = null,
blockchain = data.walletManager.wallet.blockchain,
) ?: return null
)
val command = SignHashCommand(
hash = dataToSign.hash,

View file

@ -26,6 +26,7 @@ internal fun StoriesButton(
showProgress = showProgress,
enabled = true,
shape = TangemTheme.shapes.roundedCornersXMedium,
textStyle = TangemTheme.typography.subtitle1,
iconPadding = when (icon) {
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing4
is TangemButtonIconPosition.End,

View file

@ -135,4 +135,13 @@ interface TangemTechApi {
@ReadTimeout(duration = 5, unit = TimeUnit.SECONDS)
@GET("networks/providers")
suspend fun getBlockchainProviders(): Map<String, List<ProviderModel>>
companion object {
val marketsQuoteFields = listOf(
"price",
"priceChange24h",
"priceChange1w",
"priceChange30d",
)
}
}

View file

@ -1,8 +0,0 @@
package com.tangem.datasource.di
import javax.inject.Qualifier
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class DevTangemApi

View file

@ -35,7 +35,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal object NetworkModule {
private const val DEV_V1_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/"
private const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/"
private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L
@ -128,44 +127,27 @@ internal object NetworkModule {
}
@Provides
@DevTangemApi
@Singleton
fun provideTangemTechDevApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appVersionProvider: AppVersionProvider,
): TangemTechApi {
return provideTangemTechApiInternal(
moshi = moshi,
context = context,
appVersionProvider = appVersionProvider,
baseUrl = DEV_V1_TANGEM_TECH_BASE_URL,
)
}
// TODO: [REDACTED_JIRA]
@Provides
@DevTangemApi
@Singleton
fun provideTangemTechMarketsApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appVersionProvider: AppVersionProvider,
apiConfigsManager: ApiConfigsManager,
): TangemTechMarketsApi {
return provideTangemTechApiInternal(
return createApi(
id = ApiConfig.ID.TangemTech,
moshi = moshi,
context = context,
appVersionProvider = appVersionProvider,
baseUrl = DEV_V1_TANGEM_TECH_BASE_URL,
timeouts = Timeouts(
callTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS,
connectTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS,
readTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS,
),
requestHeaders = listOf(AppVersionPlatformHeaders(appVersionProvider)),
apiConfigsManager = apiConfigsManager,
clientBuilder = {
this.callTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.connectTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.readTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.applyTimeoutAnnotations()
},
)
}
@Deprecated("use createApi instead")
private inline fun <reified T> provideTangemTechApiInternal(
moshi: Moshi,
context: Context,

View file

@ -99,7 +99,7 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
scope.launch {
context.actionsFlow
.conflate()
.buffer()
.collect { action ->
collectActions(action)
}

View file

@ -383,6 +383,7 @@
<string name="markets_token_details_blockchain_site">Webseite</string>
<string name="markets_token_details_buy_pressure">Kaufdruck</string>
<string name="markets_token_details_buy_pressure_description">Die Differenz zwischen Käufer- und Verkäufervolumen</string>
<string name="markets_token_details_buy_pressure_full">Kaufdruck</string>
<string name="markets_token_details_circulating_supply">Umlaufmenge</string>
<string name="markets_token_details_circulating_supply_description">Die Gesamtzahl der Coins, die für den Handel verfügbar sind und auf dem Markt zirkulieren</string>
<string name="markets_token_details_experienced_buyers">Erfahrene Käufer</string>
@ -650,6 +651,8 @@
<string name="staking_amount_requirement_error">Die Anzahl der zu stakenden Krypros muss mindesten %s betragen</string>
<string name="staking_amount_tron_integer_error">Der Stakingbetrag wird aufgrund der Netzwerkregeln auf %1$s TRX aufgerundet.</string>
<string name="staking_claim_unstaked">Nicht gestakte beanspruche</string>
<string name="staking_details_account_fee">Gebühr für das Staking-Konto</string>
<string name="staking_details_account_fee_info">Ein Staking-Konto ist ein spezielles Konto, auf dem eingesetzte SOL-Token gelagert werden. Es wird erstellt, wenn du deine Token an einen Validator delegierst, um an der Transaktionsvalidierung teilzunehmen und Belohnungen zu verdienen. Für die Erstellung des Staking-Kontos wird eine geringe Gebühr erhoben, die nach Abschluss des Stakings zurückgegeben wird.</string>
<string name="staking_details_annual_percentage_rate">Jährliche prozentuale Rendite</string>
<string name="staking_details_annual_percentage_rate_info">Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst.</string>
<string name="staking_details_apr">Effektiver Jahreszins</string>
@ -721,6 +724,7 @@
<string name="staking_transaction_in_progress_text">Die Transaktion wird bearbeitet! Derzeit findet eine Validierung in der Blockchain statt. Dies kann einige Minuten dauern.</string>
<string name="staking_unbonding">Lösen der Bindungen</string>
<string name="staking_unlocked_locked">Gelocktes unlocken</string>
<string name="staking_unlocking">Entsperren</string>
<string name="staking_unstaked">Unstaken</string>
<string name="staking_unstaking">Staking beenden</string>
<string name="staking_unstaking_item_name">Unstake Assets %s</string>
@ -863,6 +867,8 @@
<string name="warning_backup_errors_title">Aktivierungsfehler</string>
<string name="warning_beacon_chain_retirement_content">Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen.</string>
<string name="warning_beacon_chain_retirement_title">BNB Beacon Chain wird abgeschaltet</string>
<string name="warning_blocked_funds_for_fee_message">Bitte zahle ein paar %1$s ein, um die Netzwerkgebühr zu decken</string>
<string name="warning_blocked_funds_for_fee_title">Unzureichende Mittel zur Deckung der Netzgebühr</string>
<string name="warning_button_could_be_better">Ausbaufähig</string>
<string name="warning_button_like_it">Gefällt mir</string>
<string name="warning_button_ok">OK, habe ich verstanden!</string>

View file

@ -382,6 +382,7 @@
<string name="markets_token_details_blockchain_site">Sitio web</string>
<string name="markets_token_details_buy_pressure">Presión de compra</string>
<string name="markets_token_details_buy_pressure_description">La diferencia entre el volumen de compradores y el volumen de vendedores</string>
<string name="markets_token_details_buy_pressure_full">Presión de compra</string>
<string name="markets_token_details_circulating_supply">Suministro circulante</string>
<string name="markets_token_details_circulating_supply_description">El número total de monedas que están disponibles para el comercio y que circulan en el mercado.</string>
<string name="markets_token_details_experienced_buyers">Compradores experimentados</string>

View file

@ -382,6 +382,7 @@
<string name="markets_token_details_blockchain_site">Site web</string>
<string name="markets_token_details_buy_pressure">Pression d\'achat</string>
<string name="markets_token_details_buy_pressure_description">La différence entre le volume des acheteurs et le volume des vendeurs</string>
<string name="markets_token_details_buy_pressure_full">Pression d\'achat</string>
<string name="markets_token_details_circulating_supply">Approvisionnement en circulation</string>
<string name="markets_token_details_circulating_supply_description">Le nombre total de pièces disponibles pour le trading et en circulation sur le marché</string>
<string name="markets_token_details_experienced_buyers">Acheteurs expérimentés</string>

View file

@ -353,6 +353,7 @@
<string name="markets_loading_error_title">データを読み込めません…</string>
<string name="markets_loading_no_data_title">データなし</string>
<string name="markets_quick_actions">クイックアクション</string>
<string name="markets_search_header_title">マーケットから探す</string>
<string name="markets_search_result_title">結果</string>
<string name="markets_search_see_tokens_under_100k">時価総額10万ドル以下のトークンを見る</string>
<string name="markets_search_show_tokens">トークンを表示</string>
@ -379,6 +380,7 @@
<string name="markets_token_details_blockchain_site">ウェブサイト</string>
<string name="markets_token_details_buy_pressure">買い圧力</string>
<string name="markets_token_details_buy_pressure_description">買い手と売り手の取引量の差</string>
<string name="markets_token_details_buy_pressure_full">買い圧力</string>
<string name="markets_token_details_circulating_supply">循環供給量</string>
<string name="markets_token_details_circulating_supply_description">取引可能で市場に流通しているコインの総数</string>
<string name="markets_token_details_experienced_buyers">経験豊富な買い手</string>
@ -640,6 +642,8 @@
<string name="staking_amount_requirement_error">ステーキング金額は %s 以上である必要があります</string>
<string name="staking_amount_tron_integer_error">ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。</string>
<string name="staking_claim_unstaked">ステーキング解除分を請求する</string>
<string name="staking_details_account_fee">ステーキングアカウント手数料</string>
<string name="staking_details_account_fee_info">ステーキングアカウントは、ステーキングされたSOLトークンが保管される特別なアカウントです。取引の検証に参加して報酬を得るために、トークンをバリデーターに委任すると、このアカウントが作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、この手数料はステーキングが完了すると返金されます。</string>
<string name="staking_details_annual_percentage_rate">年率</string>
<string name="staking_details_annual_percentage_rate_info">ステーキングに参加することで得られる年間収益率。</string>
<string name="staking_details_apr">APR</string>
@ -711,6 +715,7 @@
<string name="staking_transaction_in_progress_text">取引を処理中です。現在、ブロックチェーンで検証が行われています。これには数分かかる場合があります。</string>
<string name="staking_unbonding">ステーキング解約中</string>
<string name="staking_unlocked_locked">ロック解除</string>
<string name="staking_unlocking">ロック解除中</string>
<string name="staking_unstaked">ステーキングされていない</string>
<string name="staking_unstaking">ステーキング解除</string>
<string name="staking_unstaking_item_name">ステーキング解除中の資産%s</string>
@ -853,6 +858,8 @@
<string name="warning_backup_errors_title">アクティベーションに失敗しました</string>
<string name="warning_beacon_chain_retirement_content">BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください。</string>
<string name="warning_beacon_chain_retirement_title">BNBビーコンチェーンは閉鎖されます。</string>
<string name="warning_blocked_funds_for_fee_message">ネットワーク手数料をカバーするために%1$sを入金してください</string>
<string name="warning_blocked_funds_for_fee_title">ネットワーク手数料をカバーする資金が不足しています</string>
<string name="warning_button_could_be_better">もっと良くなるはず</string>
<string name="warning_button_like_it">気に入った</string>
<string name="warning_button_ok">はい、わかりました!</string>

View file

@ -38,7 +38,7 @@
<string name="biometric_lockout_warning_title">Слишком много попыток</string>
<string name="biometric_unavailable_warning">Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона.</string>
<string name="button_start_backup_process">Начать резервное копирование</string>
<string name="buy_token_description">С помощью банковской карты или банковского аккаунта</string>
<string name="buy_token_description">Используйте банковскую карту или другие методы оплаты</string>
<plurals name="card_label_card_count">
<item quantity="one">%d карта</item>
<item quantity="few">%d карты</item>
@ -276,7 +276,7 @@
<string name="express_token_list_empty_search">Токены не найдены. Пожалуйста, попробуйте другой запрос</string>
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">ID транзакции скопирован</string>
<string name="exсhange_token_description">С другой валютой в вашем кошельке</string>
<string name="exсhange_token_description">Конвертируйте одну из ваших валют в другую</string>
<string name="feedback_data_collection_message">Информация ниже не является обязательной. Вы можете стереть её, если хотите.</string>
<string name="feedback_preface_rate_negative">Расскажите, каких функций вам не хватает, и мы постараемся вам помочь.</string>
<string name="feedback_preface_scan_failed">Скажите, пожалуйста, какая у вас карта?</string>
@ -352,10 +352,12 @@
<string name="markets_common_my_portfolio">Мой портфель</string>
<string name="markets_common_title">Рынок</string>
<string name="markets_generate_addresses_notification">Чтобы создать адреса для выбранных сетей, отсканируйте вашу карту Tangem кошелька</string>
<string name="markets_hint">Потяните вверх или коснитесь поисковой строки, чтобы добавить токен</string>
<string name="markets_insights_info_description_message">Данные раздела получены из следующих сетей: %s</string>
<string name="markets_loading_error_title">Невозможно загрузить данные</string>
<string name="markets_loading_no_data_title">Нет данных</string>
<string name="markets_quick_actions">Быстрые действия</string>
<string name="markets_search_header_title">Искать на маркете</string>
<string name="markets_search_result_title">Результат</string>
<string name="markets_search_see_tokens_under_100k">Токены с капитализацией меньше 100к USD</string>
<string name="markets_search_show_tokens">Показать токены</string>
@ -385,6 +387,7 @@
<string name="markets_token_details_blockchain_site">Веб-сайт</string>
<string name="markets_token_details_buy_pressure">Покуп. предпоч.</string>
<string name="markets_token_details_buy_pressure_description">Разница между объемом покупателей и продавцов</string>
<string name="markets_token_details_buy_pressure_full">Покуп. предпоч.</string>
<string name="markets_token_details_circulating_supply">Циркулир. предл.</string>
<string name="markets_token_details_circulating_supply_description">Общее количество монет, доступных для торговли и находящихся в обращении на рынке</string>
<string name="markets_token_details_experienced_buyers">Опытные трейдеры</string>
@ -523,7 +526,7 @@
<string name="qr_scanner_camera_denied_title">Доступ к камере запрещен</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) в сети %3$s</string>
<string name="receive_bottom_sheet_warning_message_full">Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств.</string>
<string name="receive_token_description">Покажи QR-код или поделись своим адресом</string>
<string name="receive_token_description">Переводите средства с другого кошелька или биржи</string>
<string name="referral_button_participate">Участвовать</string>
<string name="referral_error_failed_to_load_info">Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже.</string>
<string name="referral_error_failed_to_load_info_with_reason">Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже.</string>
@ -653,7 +656,9 @@
<string name="settings_forget_wallet_footer">Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова.</string>
<string name="settings_wallet_name_title">Имя</string>
<string name="staking_amount_requirement_error">Сумма для стейкинга должна быть не менее %s</string>
<string name="staking_amount_tron_integer_error">Согласно правилам сети, сумма стейкинга будет округлена до %1$sTRX.</string>
<string name="staking_claim_unstaked">Забрать средства</string>
<string name="staking_details_account_fee_info">Стейкинг аккаунт — это специальный счет, на котором хранятся застейканные токены SOL. Он создается при делегировании ваших токенов валидатору для участия в подтверждении транзакций и получении наград. За создание стейкинг аккаунта взимается небольшая комиссия, которая возвращается после завершения стейкинга.</string>
<string name="staking_details_annual_percentage_rate">Процентная ставка</string>
<string name="staking_details_annual_percentage_rate_info">Годовой процентный доход, который вы можете получить от участия в стейкинге.</string>
<string name="staking_details_apr">APR</string>
@ -697,6 +702,7 @@
<string name="staking_reward_schedule_block">Блок</string>
<string name="staking_reward_schedule_day">День</string>
<string name="staking_reward_schedule_each_day">Каждый день</string>
<string name="staking_reward_schedule_each_minute">Каждую минуту</string>
<string name="staking_reward_schedule_epoch">Эпоха</string>
<string name="staking_reward_schedule_era">Эра</string>
<string name="staking_reward_schedule_hour">Час</string>

View file

@ -120,7 +120,7 @@
<string name="common_go_to_provider">Перейти до провайдера</string>
<string name="common_go_to_token">Перейти до токену</string>
<string name="common_import">Імпортувати</string>
<string name="common_in_progress">процесі</string>
<string name="common_in_progress">В процесі</string>
<string name="common_later">Пізніше</string>
<string name="common_left">Залишилося %1$s</string>
<string name="common_locked">Заблокований</string>
@ -357,6 +357,7 @@
<string name="markets_common_my_portfolio">Моє портфоліо</string>
<string name="markets_common_title">Маркет</string>
<string name="markets_generate_addresses_notification">Щоб згенерувати адреси для обраних мереж, потрібно відсканувати свою картку Tangem</string>
<string name="markets_hint">Щоб додати токени, потягніть вгору або торкніться панелі пошуку</string>
<string name="markets_insights_info_description_message">Дані цього розділу отримані з наступних мереж: %s</string>
<string name="markets_loading_error_title">Не вдалося завантажити дані...</string>
<string name="markets_loading_no_data_title">Немає даних</string>
@ -367,13 +368,13 @@
<string name="markets_search_token_no_result_title">Жодного результату</string>
<string name="markets_select_network">Виберіть мережу</string>
<string name="markets_select_wallet">Оберіть гаманець</string>
<string name="markets_selector_interval_1m_title">1 міс.</string>
<string name="markets_selector_interval_1y_title">1 рік</string>
<string name="markets_selector_interval_24h_title">24 год.</string>
<string name="markets_selector_interval_3m_title">3 міс.</string>
<string name="markets_selector_interval_6m_title">6 міс.</string>
<string name="markets_selector_interval_1m_title">1м</string>
<string name="markets_selector_interval_1y_title">1р</string>
<string name="markets_selector_interval_24h_title">24г</string>
<string name="markets_selector_interval_3m_title">3м</string>
<string name="markets_selector_interval_6m_title">6м</string>
<string name="markets_selector_interval_7d_title"></string>
<string name="markets_selector_interval_all_title">Увесь</string>
<string name="markets_selector_interval_all_title">Усе</string>
<string name="markets_sort_by_experienced_buyers_title">Досвідчені покупці</string>
<string name="markets_sort_by_rating_title">За рейтингом</string>
<string name="markets_sort_by_title">Сортувати за</string>
@ -390,11 +391,12 @@
<string name="markets_token_details_blockchain_site">Веб-сайт</string>
<string name="markets_token_details_buy_pressure">Давлення покупця</string>
<string name="markets_token_details_buy_pressure_description">Різниця між обсягом покупців та обсягом продавців</string>
<string name="markets_token_details_buy_pressure_full">Давлення покупця</string>
<string name="markets_token_details_circulating_supply">Циркуляційний запас</string>
<string name="markets_token_details_circulating_supply_description">Загальна кількість монет, які доступні для торгівлі та перебувають в обігу на ринку</string>
<string name="markets_token_details_experienced_buyers">Досвідчені покупці</string>
<string name="markets_token_details_experienced_buyers_description">Мережеві покупці з додатковою вимогою мати не менше 100 вихідних транзакцій</string>
<string name="markets_token_details_fully_diluted_valuation">Повністю розведена ринкова капіталізація</string>
<string name="markets_token_details_fully_diluted_valuation">Повн. розведена кап.</string>
<string name="markets_token_details_fully_diluted_valuation_description">Загальна теоретична вартість криптовалюти, якщо всі монети, які могли б існувати, перебувають в обігу, включаючи ті, що не перебувають в обігу в даний час</string>
<string name="markets_token_details_genesis_date">Дата створення</string>
<string name="markets_token_details_high">Високий</string>
@ -661,6 +663,7 @@
<string name="settings_forget_wallet_footer">Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову.</string>
<string name="settings_wallet_name_title">Ім\'я</string>
<string name="staking_amount_requirement_error">Сума для стейкінгу має бути не менше %s</string>
<string name="staking_amount_tron_integer_error">Сума стейкінгу буде округлена до %1$s TRX відповідно до правил мережі.</string>
<string name="staking_claim_unstaked">Зняти кошти</string>
<string name="staking_details_annual_percentage_rate">Процентна ставка</string>
<string name="staking_details_annual_percentage_rate_info">Річний відсоток, який ви можете отримати, беручи участь у стейкінгу.</string>
@ -733,6 +736,7 @@
<string name="staking_transaction_in_progress_text">Транзакція обробляється! Наразі триває перевірка в блокчейні. Це може зайняти кілька хвилин.</string>
<string name="staking_unbonding">Розблокування</string>
<string name="staking_unlocked_locked">Розблокувати</string>
<string name="staking_unlocking">Розблокування</string>
<string name="staking_unstaked">Вивід зі стейкінгу</string>
<string name="staking_unstaking">Зняти зі стейкінгу</string>
<string name="staking_unstaking_item_name">Виведення зі стейкінгу %s</string>
@ -875,6 +879,8 @@
<string name="warning_backup_errors_title">Помилка активації</string>
<string name="warning_beacon_chain_retirement_content">За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain.</string>
<string name="warning_beacon_chain_retirement_title">Відключення мережі BNB Beacon Chain</string>
<string name="warning_blocked_funds_for_fee_message">Будь ласка, поповніть рахунок на %1$s для покриття комісії мережі</string>
<string name="warning_blocked_funds_for_fee_title">Недостатньо коштів для покриття комісії мережі</string>
<string name="warning_button_could_be_better">Можна краще</string>
<string name="warning_button_like_it">Вподобати</string>
<string name="warning_button_ok">Зрозуміло!</string>

View file

@ -39,7 +39,7 @@
<string name="biometric_lockout_warning_title">Too many attempts</string>
<string name="biometric_unavailable_warning">You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings.</string>
<string name="button_start_backup_process">Start backup process</string>
<string name="buy_token_description">With your bank card or bank account</string>
<string name="buy_token_description">Use a bank card or other payment methods</string>
<plurals name="card_label_card_count">
<item quantity="one">%d card</item>
<item quantity="other">%d cards</item>
@ -275,7 +275,7 @@
<string name="express_token_list_empty_search">No tokens found. Please try another request</string>
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">Transaction ID copied</string>
<string name="exсhange_token_description">With another currency in your wallet</string>
<string name="exсhange_token_description">Convert one of your currencies to another</string>
<string name="feedback_data_collection_message">The following information is optional. You can erase it if you don\'t want to share it.</string>
<string name="feedback_preface_rate_negative">Tell us what functions you are missing, and we will try to help you.</string>
<string name="feedback_preface_scan_failed">Please tell us what card do you have</string>
@ -356,6 +356,7 @@
<string name="markets_loading_error_title">Unable to load the data…</string>
<string name="markets_loading_no_data_title">No data</string>
<string name="markets_quick_actions">Quick actions</string>
<string name="markets_search_header_title">Search through the market</string>
<string name="markets_search_result_title">Result</string>
<string name="markets_search_see_tokens_under_100k">See tokens under 100k USD market cap</string>
<string name="markets_search_show_tokens">Show tokens</string>
@ -383,6 +384,7 @@
<string name="markets_token_details_blockchain_site">Website</string>
<string name="markets_token_details_buy_pressure">Buy pressure</string>
<string name="markets_token_details_buy_pressure_description">The difference between buyers volume and sellers volume</string>
<string name="markets_token_details_buy_pressure_full">Buy pressure</string>
<string name="markets_token_details_circulating_supply">Circulating supply</string>
<string name="markets_token_details_circulating_supply_description">The total number of coins that are available for trading and are circulating in the market</string>
<string name="markets_token_details_experienced_buyers">Experienced buyers</string>
@ -517,7 +519,7 @@
<string name="qr_scanner_camera_denied_title">Camera access denied</string>
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) on %3$s network</string>
<string name="receive_bottom_sheet_warning_message_full">Send only %s to this address. Sending any other currency will result in its irreversible loss.</string>
<string name="receive_token_description">Show a QR-code or share your address</string>
<string name="receive_token_description">Transfer funds from another wallet or exchange</string>
<string name="referral_button_participate">Participate</string>
<string name="referral_error_failed_to_load_info">Failed to load the information about the referral program. Please try again later.</string>
<string name="referral_error_failed_to_load_info_with_reason">Failed to load the information about the referral program. Error code: %s. Please try again later.</string>
@ -567,7 +569,7 @@
<string name="scan_card_settings_message">Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.</string>
<string name="scan_card_settings_title">Get your card ready!</string>
<string name="send_additional_field_already_included">Already included in the entered address</string>
<string name="send_alert_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
<string name="send_alert_fee_too_high_text">Your commission amount is %s times higher than the recommended amount. Please review and adjust your custom settings.</string>
<string name="send_alert_fee_too_low_text">You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue?</string>
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode: %2$s</string>
<string name="send_alert_transaction_failed_title">The transaction is not completed</string>
@ -602,7 +604,7 @@
<string name="send_notification_exceed_balance_title">Total exceeds balance</string>
<string name="send_notification_existential_deposit_text">A balance of at least %s is required to keep your account on the blockchain to prevent security risks. This amount will remain in your balance and cannot be withdrawn.</string>
<string name="send_notification_existential_deposit_title">Existential deposit</string>
<string name="send_notification_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
<string name="send_notification_fee_too_high_text">Your commission amount is %s times higher than the recommended amount. Please review and adjust your custom settings.</string>
<string name="send_notification_fee_too_high_title">Custom fee is high</string>
<string name="send_notification_high_fee_text">Due to the peculiarities of the %1$s network, the fee for transferring the entire balance is higher. To reduce the commission, you can leave %2$s.</string>
<string name="send_notification_high_fee_title">The fee is higher</string>
@ -648,6 +650,8 @@
<string name="staking_amount_requirement_error">The amount to stake must be at least %s</string>
<string name="staking_amount_tron_integer_error">Staking amount will be rounded to %1$s TRX due to network rules.</string>
<string name="staking_claim_unstaked">Claim unstaked</string>
<string name="staking_details_account_fee">Stake account fee</string>
<string name="staking_details_account_fee_info">A staking account is a special account where staked SOL tokens are stored. It is created when you delegate your tokens to a validator to participate in transaction validation and earn rewards. A small fee is charged for creating the staking account, which is returned after the staking is completed.</string>
<string name="staking_details_annual_percentage_rate">Annual percentage rate</string>
<string name="staking_details_annual_percentage_rate_info">The annual percentage return you can earn from participating in staking.</string>
<string name="staking_details_apr">APR</string>
@ -675,6 +679,7 @@
<string name="staking_migrate">Migrate</string>
<string name="staking_native">Native staking</string>
<string name="staking_notification_claim_rewards_text">Earned rewards will be sent to your wallet and available for use immediately</string>
<string name="staking_notification_earn_rewards_text">Stake securely and start earning your rewards</string>
<string name="staking_notification_earn_rewards_text_daily">Stake securely and start earning daily rewards</string>
<string name="staking_notification_earn_rewards_text_hourly">Stake securely and start earning hourly rewards</string>
<string name="staking_notification_earn_rewards_text_monthly">Stake securely and start earning monthly rewards</string>
@ -703,6 +708,7 @@
<string name="staking_reward_schedule_block">Block</string>
<string name="staking_reward_schedule_day">Day</string>
<string name="staking_reward_schedule_each_day">Each day</string>
<string name="staking_reward_schedule_each_minute">Each minute</string>
<string name="staking_reward_schedule_epoch">Epoch</string>
<string name="staking_reward_schedule_era">Era</string>
<string name="staking_reward_schedule_hour">Hour</string>
@ -719,6 +725,7 @@
<string name="staking_transaction_in_progress_text">The transaction is being processed! Validation is currently underway in the blockchain. This may take a few minutes.</string>
<string name="staking_unbonding">Unbonding</string>
<string name="staking_unlocked_locked">Unlock</string>
<string name="staking_unlocking">Unlocking</string>
<string name="staking_unstaked">Unstaked</string>
<string name="staking_unstaking">Unstaking</string>
<string name="staking_unstaking_item_name">Unstaking assets %s</string>

View file

@ -14,8 +14,8 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.*
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
// region TextButton
/**
@ -102,6 +102,7 @@ fun PrimaryButton(
enabled = enabled,
showProgress = showProgress,
size = size,
textStyle = TangemTheme.typography.subtitle1,
)
}
@ -127,6 +128,7 @@ fun PrimaryButtonIconEnd(
enabled = enabled,
showProgress = showProgress,
size = size,
textStyle = TangemTheme.typography.subtitle1,
)
}
@ -153,6 +155,7 @@ fun PrimaryButtonIconEndTwoLines(
showProgress = showProgress,
additionalText = additionalText,
size = TangemButtonSize.TwoLines,
textStyle = TangemTheme.typography.subtitle1,
)
}
@ -176,6 +179,7 @@ fun PrimaryButtonIconStart(
colors = TangemButtonsDefaults.primaryButtonColors,
enabled = enabled,
showProgress = showProgress,
textStyle = TangemTheme.typography.subtitle1,
)
}
// endregion PrimaryButton
@ -201,6 +205,7 @@ fun SecondaryButton(
showProgress = showProgress,
size = size,
shape = shape,
textStyle = TangemTheme.typography.subtitle1,
)
}
@ -224,6 +229,7 @@ fun SecondaryButtonIconEnd(
colors = TangemButtonsDefaults.secondaryButtonColors,
enabled = enabled,
showProgress = showProgress,
textStyle = TangemTheme.typography.subtitle1,
)
}
@ -247,6 +253,7 @@ fun SecondaryButtonIconStart(
colors = TangemButtonsDefaults.secondaryButtonColors,
enabled = enabled,
showProgress = showProgress,
textStyle = TangemTheme.typography.subtitle1,
)
}
// endregion SecondaryButton

View file

@ -39,6 +39,7 @@ import com.tangem.core.ui.res.TangemThemePreview
* @param captionColor caption text color
* @param isGrayscaleImage whether to display grayscale image
* @param iconEndRes icon to end of row
* @param onImageError composable to show if image loading failed
* @param extraContent extra content
*/
@Composable
@ -53,6 +54,7 @@ internal fun InputRowImageBase(
iconTint: Color = TangemTheme.colors.icon.informative,
isGrayscaleImage: Boolean = false,
iconEndRes: Int? = null,
onImageError: (@Composable () -> Unit)? = null,
extraContent: (@Composable RowScope.() -> Unit)? = null,
) {
Row(
@ -63,13 +65,13 @@ internal fun InputRowImageBase(
InputRowAsyncImage(
imageUrl = imageUrl,
isGrayscale = isGrayscaleImage,
onImageError = onImageError,
modifier = Modifier
.size(TangemTheme.dimens.spacing36)
.clip(TangemTheme.shapes.roundedCornersXLarge),
)
SpacerW12()
}
if (iconRes != null) {
} else if (iconRes != null) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier

View file

@ -35,6 +35,7 @@ import com.tangem.core.ui.res.TangemThemePreview
* @param captionColor caption text color
* @param isGrayscaleImage whether to display grayscale image
* @param iconEndRes icon to end of row
* @param onImageError composable to show if image loading failed
*/
@Suppress("LongParameterList")
@Composable
@ -52,6 +53,7 @@ fun InputRowImageInfo(
iconTint: Color = TangemTheme.colors.icon.informative,
isGrayscaleImage: Boolean = false,
@DrawableRes iconEndRes: Int? = null,
onImageError: (@Composable () -> Unit)? = null,
) {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6),
@ -75,6 +77,7 @@ fun InputRowImageInfo(
captionColor = captionColor,
isGrayscaleImage = isGrayscaleImage,
iconEndRes = iconEndRes,
onImageError = onImageError,
) {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),

View file

@ -41,6 +41,7 @@ import java.math.BigDecimal
* @param captionColor caption text color
* @param isSelected true if selected
* @param selectorContent selector content
* @param onImageError composable to show if image loading failed
*/
@Composable
fun InputRowImageSelector(
@ -52,6 +53,7 @@ fun InputRowImageSelector(
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
isSelected: Boolean = false,
onImageError: (@Composable () -> Unit)? = null,
selectorContent: @Composable ((isSelected: Boolean, isEnabled: Boolean, onSelect: () -> Unit) -> Unit),
) {
InputRowImageBase(
@ -60,6 +62,7 @@ fun InputRowImageSelector(
imageUrl = imageUrl,
subtitleColor = subtitleColor,
captionColor = captionColor,
onImageError = onImageError,
modifier = modifier
.clickable(
onClick = onSelect,

View file

@ -18,9 +18,15 @@ import com.tangem.core.ui.utils.getGreyScaleColorFilter
* @param imageUrl url of the image
* @param modifier modifier
* @param isGrayscale whether to apply grayscale filter
* @param onImageError composable to show if image loading failed
*/
@Composable
internal fun InputRowAsyncImage(imageUrl: String, modifier: Modifier = Modifier, isGrayscale: Boolean = false) {
internal fun InputRowAsyncImage(
imageUrl: String,
modifier: Modifier = Modifier,
isGrayscale: Boolean = false,
onImageError: (@Composable () -> Unit)? = null,
) {
val (alpha, colorFilter) = getGreyScaleColorFilter(isGrayscale = isGrayscale)
SubcomposeAsyncImage(
modifier = modifier,
@ -33,7 +39,7 @@ internal fun InputRowAsyncImage(imageUrl: String, modifier: Modifier = Modifier,
.build(),
loading = { LoadingIcon() },
error = {
Box(
onImageError?.invoke() ?: Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.tertiary,

View file

@ -4,8 +4,13 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.*
@Composable
fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buffer: Int = 2) {
val loadMore by remember {
fun InfiniteListHandler(
listState: LazyListState,
onLoadMore: () -> Boolean,
buffer: Int = 2,
triggerLoadMoreCheckOnItemsCountChange: Boolean = false,
) {
val loadMore by remember(buffer, listState) {
derivedStateOf {
val layoutInfo = listState.layoutInfo
val totalItemsNumber = layoutInfo.totalItemsCount
@ -15,12 +20,18 @@ fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buf
}
}
val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } }
var emitted by remember(totalItemsCount) { mutableStateOf(false) }
val totalItemsCount by remember(listState) { derivedStateOf { listState.layoutInfo.totalItemsCount } }
var emitted by remember(totalItemsCount, buffer, listState) { mutableStateOf(false) }
LaunchedEffect(loadMore) {
if (loadMore && !emitted) {
emitted = onLoadMore()
}
}
LaunchedEffect(totalItemsCount) {
if (triggerLoadMoreCheckOnItemsCountChange && loadMore && !emitted) {
emitted = onLoadMore()
}
}
}

View file

@ -7,9 +7,9 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ripple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -132,17 +132,6 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier =
)
val isDarkMode = LocalIsInDarkTheme.current
TangemButton(
text = button?.text?.resolveReference().orEmpty(),
icon = TangemButtonIconPosition.Start(button?.iconResId ?: R.drawable.ic_exchange_vertical_24),
onClick = button?.onClick ?: {},
colors = TangemButtonColors(
backgroundColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary,
contentColor = Dark6,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
),
enabled = true,
showProgress = false,
modifier = Modifier.constrainAs(buttonRef) {
start.linkTo(parent.start, spacing12)
end.linkTo(parent.end, spacing12)
@ -154,6 +143,18 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier =
Visibility.Visible
}
},
text = button?.text?.resolveReference().orEmpty(),
icon = TangemButtonIconPosition.Start(button?.iconResId ?: R.drawable.ic_exchange_vertical_24),
onClick = button?.onClick ?: {},
colors = TangemButtonColors(
backgroundColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary,
contentColor = Dark6,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
),
textStyle = TangemTheme.typography.subtitle1,
enabled = true,
showProgress = false,
)
}
}

View file

@ -110,6 +110,13 @@ private fun Button(config: NotificationConfig) {
button?.let {
val isDarkMode = LocalIsInDarkTheme.current
TangemButton(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
text = button.text.resolveReference(),
icon = TangemButtonIconPosition.Start(button.iconResId ?: R.drawable.ic_exchange_vertical_24),
onClick = button.onClick,
@ -119,15 +126,9 @@ private fun Button(config: NotificationConfig) {
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
),
textStyle = TangemTheme.typography.subtitle1,
enabled = true,
showProgress = false,
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
)
}
}

View file

@ -138,7 +138,7 @@ fun String.parseToBigDecimal(decimals: Int): BigDecimal {
}
}
fun BigDecimal.parseBigDecimal(decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN): String {
fun BigDecimal.parseBigDecimal(decimals: Int, roundingMode: RoundingMode = RoundingMode.HALF_UP): String {
val decimalFormat = DecimalFormat().apply {
decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
isParseBigDecimal = true

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="18dp" android:viewportHeight="18" android:viewportWidth="18" android:width="18dp">
<path android:fillColor="#C9C9C9" android:fillType="evenOdd" android:pathData="M7.235,2.66C8.331,2.044 9.669,2.044 10.765,2.66L16.641,5.965C16.924,6.125 17.1,6.425 17.1,6.75C17.1,7.075 16.924,7.375 16.641,7.534L10.765,10.84C9.669,11.456 8.331,11.456 7.235,10.84L1.359,7.534C1.075,7.375 0.9,7.075 0.9,6.75C0.9,6.425 1.075,6.125 1.359,5.965L7.235,2.66ZM9.882,4.229C9.334,3.921 8.665,3.921 8.117,4.229L3.636,6.75L8.117,9.271C8.665,9.579 9.334,9.579 9.882,9.271L14.364,6.75L9.882,4.229Z"/>
<path android:fillColor="#C9C9C9" android:pathData="M8.117,4.229C8.665,3.921 9.334,3.921 9.882,4.229L14.364,6.75L9.882,9.271C9.334,9.579 8.665,9.579 8.117,9.271L3.636,6.75L8.117,4.229Z"/>
<path android:fillColor="#C9C9C9" android:fillType="evenOdd" android:pathData="M1.016,10.809C1.259,10.375 1.808,10.222 2.241,10.465L8.118,13.771C8.666,14.079 9.335,14.079 9.883,13.771L15.759,10.465C16.192,10.222 16.741,10.375 16.985,10.809C17.228,11.242 17.074,11.79 16.641,12.034L10.765,15.34C9.669,15.956 8.331,15.956 7.235,15.34L1.359,12.034C0.926,11.79 0.772,11.242 1.016,10.809Z"/>
</vector>

View file

@ -9,14 +9,17 @@ import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetwork
import com.tangem.data.common.utils.retryOnError
import com.tangem.data.markets.analytics.MarketsDataAnalyticsEvent
import com.tangem.data.markets.converters.*
import com.tangem.data.markets.converters.TokenChartConverter
import com.tangem.data.markets.converters.TokenMarketInfoConverter
import com.tangem.data.markets.converters.TokenMarketListConverter
import com.tangem.data.markets.converters.TokenQuotesShortConverter
import com.tangem.data.markets.converters.toRequestParam
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechApi.Companion.marketsQuoteFields
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.markets.*
@ -188,16 +191,24 @@ internal class DefaultMarketsTokenRepository(
return TokenMarketInfoConverter.convert(resultResponse)
}
override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes {
// TODO change method when backend is ready
// add error analytics event
val response = marketsApi.getCoinMarketData(
currency = fiatCurrencyCode,
coinId = tokenId,
language = "en",
override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String, tokenSymbol: String): TokenQuotes {
// for second markets iteration we should use extended api method with all required fields
val response = tangemTechApi.getQuotes(
currencyId = fiatCurrencyCode,
coinIds = tokenId,
fields = marketsQuoteFields.joinToString(separator = ","),
)
return TokenMarketInfoConverter.convert(response.getOrThrow()).quotes
val result = catchApiErrorAndSendEvent(
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
tokenSymbol = tokenSymbol,
),
) {
response.getOrThrow()
}
return TokenQuotesShortConverter.convert(tokenId, result).toFull()
}
override suspend fun createCryptoCurrency(

View file

@ -9,6 +9,7 @@ import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechApi.Companion.marketsQuoteFields
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.TokenMarketUpdateRequest
import com.tangem.pagination.Batch
@ -70,7 +71,7 @@ internal class MarketsBatchUpdateFetcher(
tangemTechApi.getQuotes(
currencyId = updateRequest.currencyId,
coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","),
fields = quoteFields.joinToString(separator = ","),
fields = marketsQuoteFields.joinToString(separator = ","),
).getOrThrow()
}
}
@ -120,13 +121,4 @@ internal class MarketsBatchUpdateFetcher(
throw e
}
}
companion object {
private val quoteFields = listOf(
"price",
"priceChange24h",
"priceChange1w",
"priceChange30d",
)
}
}

View file

@ -4,7 +4,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.markets.DefaultMarketsTokenRepository
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.DevTangemApi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -21,8 +20,8 @@ internal object MarketsDataModule {
@Provides
@Singleton
fun provideMarketsRepository(
@DevTangemApi marketsApi: TangemTechMarketsApi,
@DevTangemApi tangemTechApi: TangemTechApi,
marketsApi: TangemTechMarketsApi,
tangemTechApi: TangemTechApi,
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,

View file

@ -61,7 +61,7 @@ object TapWorkarounds {
)
// TODO try remove if it's possible because we can configure on init sdk com.tangem.common.core.Config
private val excludedBatches = listOf("0027", "0030", "0031", "0035", "DA88")
private val excludedBatches = listOf("0027", "0030", "0031", "0035", "DA88", "AF56")
private val excludedIssuers = listOf("TTM BANK")

View file

@ -11,4 +11,17 @@ data class TokenQuotes(
val m6ChangePercent: BigDecimal?,
val yearChangePercent: BigDecimal?,
val allTimeChangePercent: BigDecimal?,
)
)
fun TokenQuotes.populateWith(quotes: TokenQuotes): TokenQuotes {
return TokenQuotes(
currentPrice = quotes.currentPrice,
h24ChangePercent = quotes.h24ChangePercent ?: this.h24ChangePercent,
weekChangePercent = quotes.weekChangePercent ?: this.weekChangePercent,
monthChangePercent = quotes.monthChangePercent ?: this.monthChangePercent,
m3ChangePercent = quotes.m3ChangePercent ?: this.m3ChangePercent,
m6ChangePercent = quotes.m6ChangePercent ?: this.m6ChangePercent,
yearChangePercent = quotes.yearChangePercent ?: this.yearChangePercent,
allTimeChangePercent = quotes.allTimeChangePercent ?: this.allTimeChangePercent,
)
}

View file

@ -7,4 +7,15 @@ data class TokenQuotesShort(
val h24ChangePercent: BigDecimal,
val weekChangePercent: BigDecimal,
val monthChangePercent: BigDecimal,
)
fun TokenQuotesShort.toFull() = TokenQuotes(
currentPrice = currentPrice,
h24ChangePercent = h24ChangePercent,
weekChangePercent = weekChangePercent,
monthChangePercent = monthChangePercent,
m3ChangePercent = null,
m6ChangePercent = null,
yearChangePercent = null,
allTimeChangePercent = null,
)

View file

@ -7,11 +7,16 @@ import com.tangem.domain.markets.repositories.MarketsTokenRepository
class GetTokenFullQuotesUseCase(
private val marketsTokenRepository: MarketsTokenRepository,
) {
suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either<Unit, TokenQuotes> {
suspend operator fun invoke(
appCurrency: AppCurrency,
tokenId: String,
tokenSymbol: String,
): Either<Unit, TokenQuotes> {
return Either.catch {
marketsTokenRepository.getTokenQuotes(
fiatCurrencyCode = appCurrency.code,
tokenId = tokenId,
tokenSymbol = tokenSymbol,
)
}.mapLeft {}
}

View file

@ -33,7 +33,7 @@ interface MarketsTokenRepository {
languageCode: String,
): TokenMarketInfo
suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes
suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String, tokenSymbol: String): TokenQuotes
suspend fun createCryptoCurrency(
userWalletId: UserWalletId,

View file

@ -61,7 +61,7 @@ internal class CurrencyStatusOperations(
(yieldBalance as? YieldBalance.Data)?.address == status.address.defaultAddress.value
val currentYieldBalance = yieldBalance.takeIf { isCurrentAddressStaking }
return when {
ignoreQuote -> CryptoCurrencyStatus.NoQuote(
ignoreQuote || quote == null -> CryptoCurrencyStatus.NoQuote(
amount = amount,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
@ -70,15 +70,14 @@ internal class CurrencyStatusOperations(
)
currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom(
amount = amount,
fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate),
fiatRate = quote?.fiatRate,
priceChange = quote?.priceChange,
fiatAmount = calculateFiatAmountOrNull(amount, quote.fiatRate),
fiatRate = quote.fiatRate,
priceChange = quote.priceChange,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
networkAddress = status.address,
yieldBalance = currentYieldBalance,
)
quote == null -> CryptoCurrencyStatus.Loading
else -> CryptoCurrencyStatus.Loaded(
amount = amount,
fiatAmount = calculateFiatAmount(amount, quote.fiatRate),

View file

@ -7,7 +7,9 @@ import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
import arrow.core.toNonEmptyListOrNull
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.*
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
internal class TokenListSortingOperations(
@ -83,7 +85,7 @@ internal class TokenListSortingOperations(
return if (isAnyTokenLoading) {
tokens
} else {
tokens.sortedByDescending { it.value.fiatAmount ?: BigDecimal.ZERO }
tokens.sortedByDescending { it.getTotalBalance() }
.toNonEmptyListOrNull()
?: error("Tokens can not be empty here")
}
@ -92,12 +94,20 @@ internal class TokenListSortingOperations(
private fun sortGroupsByBalance(groupsWithSortedTokens: NonEmptyList<NetworkGroup>): NonEmptyList<NetworkGroup> {
return groupsWithSortedTokens
.sortedByDescending { group ->
group.currencies.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }
group.currencies.sumOf { it.getTotalBalance() }
}
.toNonEmptyListOrNull()
?: error("Tokens can not be empty here")
}
private fun CryptoCurrencyStatus.getTotalBalance(): BigDecimal {
val yieldBalance = value.yieldBalance as? YieldBalance.Data
val totalYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance().orZero()
val totalFiatYieldBalance = totalYieldBalance.multiply(value.fiatRate.orZero())
return value.fiatAmount?.plus(totalFiatYieldBalance).orZero()
}
sealed class Error {
object EmptyTokens : Error()

View file

@ -229,6 +229,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
val result = getTokenFullQuotesUseCase(
tokenId = params.token.id,
appCurrency = currentAppCurrency.value,
tokenSymbol = params.token.symbol,
)
result.onRight { res ->
@ -389,9 +390,11 @@ internal class MarketsTokenDetailsModel @Inject constructor(
}
private suspend fun updateQuotes(newQuotes: TokenQuotes) {
quotesStateUpdater.updateQuotes(newQuotes)
val populatedNewQuotes = currentQuotes.value.populateWith(newQuotes)
val percent = newQuotes
quotesStateUpdater.updateQuotes(newQuotes = populatedNewQuotes)
val percent = populatedNewQuotes
.getPercentByInterval(interval = state.value.selectedInterval)
chartDataProducer.runTransaction {

View file

@ -19,6 +19,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.util.fastForEachIndexed
@ -162,6 +163,7 @@ private fun ContinueButton(
showProgress = false,
size = TangemButtonSize.Default,
colors = TangemButtonsDefaults.primaryButtonColors,
textStyle = TangemTheme.typography.subtitle1,
onClick = onClick,
animateContentChange = true,
)
@ -200,10 +202,13 @@ private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifi
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1f, fill = false)
.alignByBaseline(),
text = state.tokenName,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
SpacerW6()
Text(
@ -213,6 +218,8 @@ private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifi
text = state.tokenCurrencySymbol,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
overflow = TextOverflow.Visible,
maxLines = 1,
)
}

View file

@ -62,14 +62,14 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider
selectedWallet = userWallet,
selectNetworkUM = SelectNetworkUM(
tokenId = "etherium",
tokenName = "Etherium",
tokenName = "Etherium Etherium Etherium Etherium",
tokenCurrencySymbol = "ETH",
networks = persistentListOf(
blockchainRow.copy(
type = "MAIN",
isMainNetwork = true,
isSelected = true,
),
).copy(name = "Etherium Etherium Etherium Etherium"),
*Array(25) { blockchainRow },
),

View file

@ -14,7 +14,6 @@ import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
@ -111,54 +110,55 @@ internal class MarketsListUMStateManager(
private fun MarketsListUM.updateItems(newItems: ImmutableList<MarketsListItemUM>): MarketsListUM {
val currentState = this
val isNextPageInSearch = isInSearchState && (this.list as? ListUM.Content)?.showUnder100kTokens == true
var searchUiItemsCached: ImmutableList<MarketsListItemUM> = persistentListOf()
val items = when {
isInSearchState && isNextPageInSearch.not() -> {
searchUiItemsCached = newItems
val filtered = newItems.filter { item -> item.isUnder100kMarketCap.not() }.toImmutableList()
if (isInSearchMode.not() || currentState.showUnder100kButtonAlreadyPressed()) {
val itemsWithFilteredPriceChange = newItems.filterPriceChangeByVisibility()
if (filtered.size == newItems.size) {
return currentState.copy(list = generalContentState(newItems))
} else {
filtered
}
}
else -> {
searchUiItemsCached = persistentListOf()
newItems
}
return currentState.copy(
list = generalContentState(itemsWithFilteredPriceChange)
.copy(
showUnder100kTokensNotificationWasHidden = currentState.showUnder100kButtonAlreadyPressed(),
),
)
}
val itemsWithFilteredPriceChange = items.filterPriceChangeByVisibility()
// Search state cases
return currentState.copy(
list = ListUM.Content(
items = itemsWithFilteredPriceChange,
loadMore = onLoadMoreUiItems,
visibleIdsChanged = visibleItemsChanged,
showUnder100kTokens = isInSearchState.not() || isNextPageInSearch,
onShowTokensUnder100kClicked = {
if (searchUiItemsCached.isNotEmpty()) {
val filtered = newItems.filter { item -> item.isUnder100kMarketCap.not() }
.toImmutableList()
.filterPriceChangeByVisibility()
if (filtered.size != newItems.size) {
val searchUiItemsCached = newItems.filterPriceChangeByVisibility()
return currentState.copy(
list = generalContentState(filtered).copy(
showUnder100kTokensNotificationWasHidden = false,
showUnder100kTokensNotification = true,
onShowTokensUnder100kClicked = {
state.update { s ->
if (s.list is ListUM.Content) {
(s.list as? ListUM.Content)?.let {
s.copy(
list = s.list.copy(
items = searchUiItemsCached,
showUnder100kTokens = true,
showUnder100kTokensNotification = false,
showUnder100kTokensNotificationWasHidden = true,
),
)
} else {
s
}
} ?: s
}
}
},
triggerScrollReset = consumedEvent(),
onItemClick = onTokenClick,
),
)
},
),
)
} else {
return currentState.copy(
list = generalContentState(newItems.filterPriceChangeByVisibility()),
)
}
}
private fun MarketsListUM.showUnder100kButtonAlreadyPressed(): Boolean {
return this.list is ListUM.Content && this.isInSearchMode && this.list.showUnder100kTokensNotificationWasHidden
}
// Show price change animation for visible items only
@ -182,10 +182,11 @@ internal class MarketsListUMStateManager(
items = newItems,
loadMore = onLoadMoreUiItems,
visibleIdsChanged = visibleItemsChanged,
showUnder100kTokens = true,
showUnder100kTokensNotification = false,
onShowTokensUnder100kClicked = {},
triggerScrollReset = consumedEvent(),
onItemClick = onTokenClick,
showUnder100kTokensNotificationWasHidden = false,
)
}

View file

@ -301,7 +301,8 @@ private fun Preview() {
item.copy(id = index.toString())
}
.toImmutableList(),
showUnder100kTokens = false,
showUnder100kTokensNotification = false,
showUnder100kTokensNotificationWasHidden = false,
loadMore = {},
visibleIdsChanged = {},
onShowTokensUnder100kClicked = {},

View file

@ -22,6 +22,7 @@ import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import kotlinx.coroutines.launch
private const val LOAD_NEXT_PAGE_ON_END_INDEX = 50
private const val LOAD_NEXT_PAGE_ON_END_INDEX_SEARCH = 25
private const val TOKEN_LAZY_LIST_ID_SEPARATOR = "***"
@Composable
@ -95,7 +96,7 @@ internal fun MarketsListLazyColumn(
)
}
if (isInSearchMode && state.showUnder100kTokens.not()) {
if (isInSearchMode && state.showUnder100kTokensNotification) {
item(key = "show tokens under 100k".hashCode()) {
ShowTokensUnder100kItem(
onShowTokensClick = state.onShowTokensUnder100kClicked,
@ -112,10 +113,15 @@ internal fun MarketsListLazyColumn(
InfiniteListHandler(
listState = lazyListState,
buffer = LOAD_NEXT_PAGE_ON_END_INDEX,
buffer = if (isInSearchMode) {
LOAD_NEXT_PAGE_ON_END_INDEX_SEARCH
} else {
LOAD_NEXT_PAGE_ON_END_INDEX
},
triggerLoadMoreCheckOnItemsCountChange = true,
onLoadMore = remember(state) {
{
if (state is ListUM.Content && state.showUnder100kTokens) {
if (state is ListUM.Content && state.showUnder100kTokensNotification.not()) {
state.loadMore()
true
} else {

View file

@ -22,6 +22,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
chardData = MarketChartRawData(
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
),
isUnder100kMarketCap = false,
),
MarketsListItemUM(
id = "1",
@ -34,6 +35,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
trendPercentText = "12.43%",
trendType = PriceChangeType.NEUTRAL,
chardData = null,
isUnder100kMarketCap = false,
),
MarketsListItemUM(
id = "1",
@ -48,6 +50,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
chardData = MarketChartRawData(
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
),
isUnder100kMarketCap = false,
),
MarketsListItemUM(
id = "1",
@ -62,6 +65,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
chardData = MarketChartRawData(
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
),
isUnder100kMarketCap = false,
),
MarketsListItemUM(
id = "1",
@ -76,6 +80,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
chardData = MarketChartRawData(
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
),
isUnder100kMarketCap = false,
),
MarketsListItemUM(
id = "1",
@ -90,6 +95,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet
chardData = MarketChartRawData(
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
),
isUnder100kMarketCap = false,
),
),
)

View file

@ -17,7 +17,7 @@ data class MarketsListItemUM(
val trendPercentText: String,
val trendType: PriceChangeType,
val chardData: MarketChartRawData?,
val isUnder100kMarketCap: Boolean = false,
val isUnder100kMarketCap: Boolean,
) {
val chartType: MarketChartLook.Type = when (trendType) {
PriceChangeType.UP -> MarketChartLook.Type.Growing

View file

@ -41,7 +41,8 @@ sealed class ListUM {
data class Content(
val items: ImmutableList<MarketsListItemUM>,
val showUnder100kTokens: Boolean,
val showUnder100kTokensNotification: Boolean,
val showUnder100kTokensNotificationWasHidden: Boolean,
val loadMore: () -> Unit,
val visibleIdsChanged: (List<String>) -> Unit,
val onShowTokensUnder100kClicked: () -> Unit,

View file

@ -127,6 +127,7 @@ private fun SendNavigationButton(
}
}
TangemButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(buttonTextId),
icon = buttonIcon,
enabled = isButtonEnabled,
@ -135,8 +136,8 @@ private fun SendNavigationButton(
buttonClick()
},
showProgress = false,
modifier = Modifier.fillMaxWidth(),
colors = TangemButtonsDefaults.primaryButtonColors,
textStyle = TangemTheme.typography.subtitle1,
)
}
}
@ -274,7 +275,7 @@ private fun isButtonEnabled(currentState: SendUiCurrentScreen, uiState: SendUiSt
private fun getTokenFeeSendingText(feeState: SendStates.FeeState, fee: Fee.Tron, sendingValue: String): TextReference {
val suffix = when {
fee.feeEnergy == 0L -> {
fee.remainingEnergy == 0L -> {
resourceReference(
R.string.send_summary_transaction_description_suffix_including,
wrappedList(feeState.getFiatValue()),

View file

@ -73,6 +73,7 @@ internal class AddStakingNotificationsTransformer(
isSubtractAvailable = isSubtractAvailable,
reduceAmountBy = reduceAmountBy,
)
val minimumRequirement = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum.orZero()
val sendingAmount = if (isEnterAction) {
checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isSubtractAvailable,
@ -84,7 +85,7 @@ internal class AddStakingNotificationsTransformer(
} else {
// No amount is taken from account balance on exit or pending actions
BigDecimal.ZERO
}
}.max(minimumRequirement)
val notifications = buildList {
// errors
@ -106,7 +107,7 @@ internal class AddStakingNotificationsTransformer(
amountState = amountState,
feeState = feeState,
sendingAmount = sendingAmount,
isFeeCoverage = isFeeCoverage && isEnterAction,
isFeeCoverage = isFeeCoverage && isEnterAction && !sendingAmount.equals(minimumRequirement),
)
addInfoNotifications(prevState)
@ -223,11 +224,10 @@ internal class AddStakingNotificationsTransformer(
cryptoCurrencyStatus: CryptoCurrencyStatus,
onClick: (CryptoCurrency) -> Unit,
) {
val minimumRequirement = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
if (!isSubtractionAvailable) return
val showNotification = sendingAmount + feeAmount > balance - minimumRequirement.orZero()
val showNotification = sendingAmount + feeAmount > balance
if (showNotification) {
val notification = if (actionType == StakingActionCommonType.ENTER) {
NotificationUM.Error.TotalExceedsBalance

View file

@ -35,7 +35,7 @@ internal class SetConfirmationStateLoadingTransformer(
availableValidators = yield.validators,
),
notifications = persistentListOf(),
footerText = getFooter(prevState),
footerText = getFooter(prevState, chosenValidator),
transactionDoneState = TransactionDoneState.Empty,
pendingAction = possibleConfirmationState?.pendingAction,
pendingActions = possibleConfirmationState?.pendingActions,
@ -45,14 +45,12 @@ internal class SetConfirmationStateLoadingTransformer(
)
}
private fun getFooter(state: StakingUiState): TextReference {
private fun getFooter(state: StakingUiState, validator: Yield.Validator): TextReference {
val amountState = state.amountState as? AmountState.Data
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
val validatorState = confirmationState?.validatorState as? ValidatorState.Content
val isEnterAction = state.actionType == StakingActionCommonType.ENTER
val apr = validatorState?.chosenValidator?.apr.orZero()
val apr = validator.apr.orZero()
val amountDecimal = amountState?.amountTextField?.fiatAmount?.value
val potentialReward = amountDecimal?.multiply(apr)

View file

@ -19,6 +19,7 @@ import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter
import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import com.tangem.lib.crypto.BlockchainUtils.isCosmos
import com.tangem.lib.crypto.BlockchainUtils.isPolkadot
import com.tangem.utils.Provider
import com.tangem.utils.isNullOrZero
@ -201,12 +202,12 @@ internal class SetInitialDataStateTransformer(
private fun createRewardScheduleItem(
rewardSchedule: Yield.Metadata.RewardSchedule,
): RoundedListWithDividersItemData? {
val endTextId = rewardScheduleResources[rewardSchedule] ?: return null
val endTextReference = getRewardScheduleText(rewardSchedule) ?: return null
return RoundedListWithDividersItemData(
id = R.string.staking_details_reward_schedule,
startText = TextReference.Res(R.string.staking_details_reward_schedule),
endText = TextReference.Res(endTextId),
startText = resourceReference(R.string.staking_details_reward_schedule),
endText = endTextReference,
iconClick = { clickIntents.onInfoClick(InfoType.REWARD_SCHEDULE) },
)
}
@ -252,21 +253,29 @@ internal class SetInitialDataStateTransformer(
return resourceReference(R.string.common_range, wrappedList(formattedMinApr, formattedMaxApr))
}
companion object {
private val EQUALITY_THRESHOLD = BigDecimal(1E-10)
private fun getRewardScheduleText(rewardSchedule: Yield.Metadata.RewardSchedule): TextReference? {
return when (rewardSchedule) {
Yield.Metadata.RewardSchedule.BLOCK -> {
val networkId = cryptoCurrencyStatusProvider().currency.network.id.value
when {
isCosmos(networkId) -> resourceReference(R.string.staking_reward_schedule_each_minute)
else -> resourceReference(R.string.staking_reward_schedule_each_day)
}
}
Yield.Metadata.RewardSchedule.WEEK -> resourceReference(R.string.staking_reward_schedule_week)
Yield.Metadata.RewardSchedule.HOUR -> resourceReference(R.string.staking_reward_schedule_hour)
Yield.Metadata.RewardSchedule.DAY -> resourceReference(R.string.staking_reward_schedule_each_day)
Yield.Metadata.RewardSchedule.MONTH -> resourceReference(R.string.staking_reward_schedule_month)
Yield.Metadata.RewardSchedule.ERA -> resourceReference(R.string.staking_reward_schedule_era)
Yield.Metadata.RewardSchedule.EPOCH -> resourceReference(R.string.staking_reward_schedule_epoch)
Yield.Metadata.RewardSchedule.UNKNOWN -> null
}
}
private val rewardScheduleResources = mapOf(
Yield.Metadata.RewardSchedule.BLOCK to R.string.staking_reward_schedule_each_day,
Yield.Metadata.RewardSchedule.WEEK to R.string.staking_reward_schedule_week,
Yield.Metadata.RewardSchedule.HOUR to R.string.staking_reward_schedule_hour,
Yield.Metadata.RewardSchedule.DAY to R.string.staking_reward_schedule_each_day,
Yield.Metadata.RewardSchedule.MONTH to R.string.staking_reward_schedule_month,
Yield.Metadata.RewardSchedule.ERA to R.string.staking_reward_schedule_era,
Yield.Metadata.RewardSchedule.EPOCH to R.string.staking_reward_schedule_epoch,
Yield.Metadata.RewardSchedule.UNKNOWN to null,
)
private companion object {
val EQUALITY_THRESHOLD = BigDecimal(1E-10)
private val rewardClaimingResources = mapOf(
val rewardClaimingResources = mapOf(
Yield.Metadata.RewardClaiming.MANUAL to R.string.staking_reward_claiming_manual,
Yield.Metadata.RewardClaiming.AUTO to R.string.staking_reward_claiming_auto,
Yield.Metadata.RewardSchedule.UNKNOWN to null,

View file

@ -31,7 +31,12 @@ internal fun checkAndCalculateSubtractedAmount(
reduceAmountBy = reduceAmountBy,
)
return if (isFeeCoverage) {
balance.minus(reduceAmountBy).minus(feeValueRounded)
val reducedAmount = balance.minus(reduceAmountBy).minus(feeValueRounded)
if (isTron(cryptoCurrencyStatus.currency.network.id.value)) {
reducedAmount.setScale(0, RoundingMode.DOWN)
} else {
reducedAmount
}
} else {
amountValue
}

View file

@ -52,6 +52,7 @@ internal fun StakingClaimRewardsValidatorContent(
infoTitle = item.fiatAmount,
infoSubtitle = item.cryptoAmount,
imageUrl = item.validator?.image.orEmpty(),
onImageError = { ValidatorImagePlaceholder() },
modifier = modifier
.roundedShapeItemDecoration(index, state.rewards.lastIndex, false)
.background(TangemTheme.colors.background.action)

View file

@ -57,7 +57,6 @@ import com.tangem.features.staking.impl.presentation.state.previewdata.InitialSt
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.StringsSigns.PLUS
import com.tangem.utils.extensions.orZero
private const val BANNER_BLOCK_KEY = "BannerBlock"
@ -231,8 +230,6 @@ private fun StakingRewardBlock(
val (text, textColor) = when (rewardBlockType) {
RewardBlockType.Rewards -> {
annotatedReference {
append(PLUS)
appendSpace()
append(rewardFiat.orMaskWithStars(isBalanceHidden))
appendSpace()
append(DOT)
@ -282,6 +279,7 @@ private fun ActiveStakingBlock(
imageUrl = balance.getImage(),
iconRes = icon,
iconTint = iconTint,
onImageError = { ValidatorImagePlaceholder() },
modifier = modifier
.background(TangemTheme.colors.background.action)
.clickable(

View file

@ -3,10 +3,7 @@ package com.tangem.features.staking.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
@ -95,6 +92,7 @@ internal fun StakingValidatorListContent(
)
}
},
onImageError = { ValidatorImagePlaceholder() },
)
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.features.staking.impl.presentation.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.staking.impl.R
@Composable
internal fun ValidatorImagePlaceholder() {
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(R.drawable.ic_staking_filled_18)),
contentDescription = null,
tint = TangemTheme.colors.icon.inactive,
modifier = Modifier
.background(TangemTheme.colors.icon.primary1, CircleShape)
.padding(TangemTheme.dimens.size9),
)
}

View file

@ -16,6 +16,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholder
import com.tangem.utils.extensions.orZero
@Composable
@ -45,6 +46,7 @@ internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit)
)
},
imageUrl = validatorState.chosenValidator.image.orEmpty(),
onImageError = { ValidatorImagePlaceholder() },
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance
@ -20,8 +21,9 @@ internal class TokenDetailsBalanceSelectStateConverter(
if (stakingBlocksState !is StakingBlockUM.Staked) return this
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() ?: return this
val stakingCryptoAmount = stakingBlocksState.cryptoAmount
val stakingFiatAmount = stakingBlocksState.fiatAmount
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data
val stakingCryptoAmount = yieldBalance?.getTotalWithRewardsStakingBalance()
val stakingFiatAmount = stakingCryptoAmount?.let { cryptoCurrencyStatus.value.fiatRate?.multiply(it) }
copy(
tokenBalanceBlockState = if (tokenBalanceBlockState is TokenDetailsBalanceBlockState.Content) {

View file

@ -21,7 +21,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getStringResourceId
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.features.tokendetails.impl.R
@ -153,8 +152,9 @@ internal class TokenDetailsLoadedBalanceConverter(
StakingBlockUM.Error(iconState = iconState)
}
else -> {
val stakingRewardAmount = yieldBalance?.getRewardStakingBalance()
val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) }
val fiatRate = status.value.fiatRate
val stakingRewardAmount = yieldBalance?.getRewardStakingBalance()?.let { fiatRate?.multiply(it) }
val stakingFiatAmount = stakingCryptoAmount?.let { fiatRate?.multiply(it) }
getStakedState(
status = status,
@ -203,7 +203,7 @@ internal class TokenDetailsLoadedBalanceConverter(
formatArgs = wrappedList(apr),
),
subtitleText = resourceReference(
id = stakingEntryInfo.rewardSchedule.getStringResourceId(),
id = R.string.staking_notification_earn_rewards_text,
formatArgs = wrappedList(stakingEntryInfo.tokenSymbol),
),
iconState = iconState,

View file

@ -8,7 +8,6 @@ import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getStringResourceId
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
@ -39,7 +38,7 @@ internal class TokenStakingStateConverter(
formatArgs = wrappedList(apr),
),
subtitleText = resourceReference(
id = stakingEntryInfo.rewardSchedule.getStringResourceId(),
id = R.string.staking_notification_earn_rewards_text,
formatArgs = wrappedList(stakingEntryInfo.tokenSymbol),
),
iconState = iconState,

View file

@ -1,26 +0,0 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.features.tokendetails.impl.R
internal fun Yield.Metadata.RewardSchedule.getStringResourceId(): Int {
return when (this) {
Yield.Metadata.RewardSchedule.BLOCK,
Yield.Metadata.RewardSchedule.DAY,
Yield.Metadata.RewardSchedule.ERA,
Yield.Metadata.RewardSchedule.EPOCH,
-> R.string.staking_notification_earn_rewards_text_daily
Yield.Metadata.RewardSchedule.HOUR,
-> R.string.staking_notification_earn_rewards_text_hourly
Yield.Metadata.RewardSchedule.WEEK,
-> R.string.staking_notification_earn_rewards_text_weekly
Yield.Metadata.RewardSchedule.MONTH,
-> R.string.staking_notification_earn_rewards_text_monthly
else
-> R.string.staking_notification_earn_rewards_text_daily
}
}

View file

@ -24,6 +24,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
@ -386,11 +387,13 @@ private inline fun BaseScaffoldWithMarkets(
LocalMainBottomSheetColor provides remember { mutableStateOf(backgroundPrimary) },
) {
val backgroundColor = LocalMainBottomSheetColor.current
var isSearchFieldFocused by remember { mutableStateOf(false) }
BottomSheetStateEffects(
bottomSheetState = bottomSheetState,
alertConfig = alertConfig,
onBottomSheetStateChange = onBottomSheetStateChange,
isSearchFieldFocused = isSearchFieldFocused,
)
TangemBottomSheetScaffold(
@ -407,7 +410,6 @@ private inline fun BaseScaffoldWithMarkets(
sheetContainerColor = backgroundColor.value,
scaffoldState = scaffoldState,
sheetPeekHeight = peekHeight,
sheetTonalElevation = 8.dp,
sheetShadowElevation = 8.dp,
sheetShape = TangemTheme.shapes.bottomSheetLarge,
sheetContent = {
@ -423,7 +425,14 @@ private inline fun BaseScaffoldWithMarkets(
modifier = Modifier.sizeIn(maxHeight = maxHeight - statusBarHeight),
) {
Hand(Modifier.drawBehind { drawRect(backgroundColor.value) })
bottomSheetContent()
Box(
modifier = Modifier
.onFocusChanged {
isSearchFieldFocused = it.isFocused
},
) {
bottomSheetContent()
}
}
},
content = { paddingValues ->
@ -462,7 +471,10 @@ private inline fun BaseScaffoldWithMarkets(
color = BottomSheetDefaults.ScrimColor,
visible = bottomSheetState.targetValue == TangemSheetValue.Expanded ||
state.showMarketsOnboarding,
onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } },
onDismissRequest = {
coroutineScope.launch { bottomSheetState.partialExpand() }
state.onDismissMarketsOnboarding()
},
)
MarketsTooltip(
@ -486,7 +498,6 @@ private inline fun BaseScaffoldWithMarkets(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MarketsTooltip(
availableHeight: Dp,
@ -640,11 +651,27 @@ private fun BottomSheetStateEffects(
bottomSheetState: TangemSheetState,
alertConfig: WalletAlertState?,
onBottomSheetStateChange: (BottomSheetState) -> Unit,
isSearchFieldFocused: Boolean,
) {
val systemUiController = rememberSystemUiController()
val navigationBarColor = TangemTheme.colors.background.primary
LaunchedEffect(key1 = bottomSheetState.targetValue, navigationBarColor) {
LaunchedEffect(navigationBarColor) {
delay(timeMillis = 100)
when (bottomSheetState.currentValue) {
TangemSheetValue.Hidden,
TangemSheetValue.Expanded,
-> systemUiController.setNavigationBarColor(
color = Color.Transparent,
darkIcons = navigationBarColor.luminance() > 0.5f,
navigationBarContrastEnforced = true,
)
TangemSheetValue.PartiallyExpanded,
-> systemUiController.setNavigationBarColor(navigationBarColor)
}
}
LaunchedEffect(bottomSheetState.targetValue, navigationBarColor) {
when (bottomSheetState.targetValue) {
TangemSheetValue.Hidden,
TangemSheetValue.Expanded,
@ -673,7 +700,7 @@ private fun BottomSheetStateEffects(
val isKeyboardVisible by rememberIsKeyboardVisible()
LaunchedEffect(isKeyboardVisible) {
if (isKeyboardVisible && alertConfig == null) {
if (isKeyboardVisible && alertConfig == null && isSearchFieldFocused) {
bottomSheetState.expand()
}
}

View file

@ -89,7 +89,7 @@ markdownComposeView = "0.5.4"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-775"
tangemBlockchainSdk = "develop-782"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-381"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^