Updated on 2026-08-14
This commit is contained in:
commit
ebe91678c7
86 changed files with 1214 additions and 553 deletions
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.google.firebase.analytics.ktx.analytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRoute.ManageTokens.Source
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
|
@ -67,7 +68,7 @@ internal class HomeViewModel @Inject constructor(
|
|||
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
|
||||
|
||||
store.dispatch(TokensAction.SetArgs.ReadAccess)
|
||||
store.dispatchNavigationAction { push(AppRoute.ManageTokens()) }
|
||||
store.dispatchNavigationAction { push(AppRoute.ManageTokens(Source.STORIES)) }
|
||||
}
|
||||
|
||||
private fun scanCard() {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.features.details.component.DetailsComponent
|
|||
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
|
||||
import com.tangem.features.managetokens.ManageTokensToggles
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
|
|
@ -112,9 +113,15 @@ internal class ChildFactory @Inject constructor(
|
|||
}
|
||||
is AppRoute.ManageTokens -> {
|
||||
if (manageTokensToggles.isFeatureEnabled) {
|
||||
val source = when (route.source) {
|
||||
AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS
|
||||
AppRoute.ManageTokens.Source.ONBOARDING -> ManageTokensSource.ONBOARDING
|
||||
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
|
||||
}
|
||||
|
||||
route.asComponentChild(
|
||||
contextProvider = contextProvider(route, contextFactory),
|
||||
params = ManageTokensComponent.Params(route.userWalletId),
|
||||
params = ManageTokensComponent.Params(route.userWalletId, source),
|
||||
componentFactory = manageTokensComponentFactory,
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -179,9 +179,16 @@ sealed class AppRoute(val path: String) : Route {
|
|||
|
||||
@Serializable
|
||||
data class ManageTokens(
|
||||
val source: Source,
|
||||
val userWalletId: UserWalletId? = null,
|
||||
) : AppRoute(path = "/manage_tokens/$userWalletId"), RouteBundleParams {
|
||||
) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/$userWalletId"), RouteBundleParams {
|
||||
override fun getBundle(): Bundle = bundle(serializer())
|
||||
|
||||
enum class Source {
|
||||
STORIES,
|
||||
ONBOARDING,
|
||||
SETTINGS,
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Analytics - Models */
|
||||
implementation(projects.core.analytics.models)
|
||||
api(projects.core.analytics.models)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.analytics)
|
||||
|
|
|
|||
|
|
@ -22,9 +22,19 @@ sealed class AnalyticsParam {
|
|||
data object Closed : RateApp("Close")
|
||||
}
|
||||
|
||||
sealed class OnOffState(val value: String) {
|
||||
data object On : OnOffState("On")
|
||||
data object Off : OnOffState("Off")
|
||||
enum class OnOffState(val value: String) {
|
||||
On("On"),
|
||||
Off("Off"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(enabled: Boolean): String {
|
||||
val state = if (enabled) On else Off
|
||||
|
||||
return state.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class OrganizeSortType(val value: String) {
|
||||
|
|
@ -135,6 +145,22 @@ sealed class AnalyticsParam {
|
|||
class SingleCurrency(currencyName: String) : WalletType(currencyName)
|
||||
}
|
||||
|
||||
enum class Validation(val value: String) {
|
||||
|
||||
OK(value = "Ok"),
|
||||
ERROR(value = "Error"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(isValid: Boolean): String {
|
||||
val status = if (isValid) OK else ERROR
|
||||
|
||||
return status.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object Key {
|
||||
const val BLOCKCHAIN = "blockchain"
|
||||
const val TOKEN_PARAM = "Token"
|
||||
|
|
@ -158,6 +184,9 @@ sealed class AnalyticsParam {
|
|||
const val VALIDATION = "Validation"
|
||||
const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host"
|
||||
const val BLOCKCHAIN_SELECTED_HOST = "selected_host"
|
||||
const val INPUT = "Input"
|
||||
const val COUNT = "Count"
|
||||
const val DERIVATION = "Derivation"
|
||||
|
||||
// region swap
|
||||
const val TOKEN_CATEGORY = "Token"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.core.analytics
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
class DummyAnalyticsEventHandler : AnalyticsEventHandler {
|
||||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
/* no-op */
|
||||
}
|
||||
}
|
||||
|
|
@ -104,8 +104,8 @@ data class TokenMarketInfoResponse(
|
|||
val marketCap: BigDecimal?,
|
||||
@Json(name = "volume_24h")
|
||||
val volume24h: BigDecimal?,
|
||||
@Json(name = "total_supply")
|
||||
val totalSupply: BigDecimal?,
|
||||
@Json(name = "max_supply")
|
||||
val maxSupply: BigDecimal?,
|
||||
@Json(name = "fully_diluted_valuation")
|
||||
val fullyDilutedValuation: BigDecimal?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -414,7 +414,8 @@
|
|||
<string name="markets_token_details_market_rating_description">Position im Krypto-Rating zwischen allen Coins basierend auf der Marktkapitalisierung</string>
|
||||
<string name="markets_token_details_market_rating_full">Marktbewertung</string>
|
||||
<string name="markets_token_details_max_supply">Maximale Versorgung</string>
|
||||
<string name="markets_token_details_max_supply_description">Leer</string>
|
||||
<string name="markets_token_details_max_supply_description">Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können</string>
|
||||
<string name="markets_token_details_max_supply_full">Maximale Versorgung</string>
|
||||
<string name="markets_token_details_metrics">Metriken</string>
|
||||
<string name="markets_token_details_official_links">Offizielle Links</string>
|
||||
<string name="markets_token_details_price_performance">Preisleistung</string>
|
||||
|
|
@ -700,6 +701,7 @@
|
|||
<string name="staking_notification_earn_rewards_text_period_week">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche.</string>
|
||||
<string name="staking_notification_earn_rewards_text_weekly">Sicher staken und wöchentliche Belohnungen verdienen.</string>
|
||||
<string name="staking_notification_earn_rewards_title">Verdiene Staking-Belohnungen</string>
|
||||
<string name="staking_notification_network_error_text">Aufgrund von Netzwerkproblemen ist Staking derzeit nicht verfügbar. Bitte versuche es später erneut.</string>
|
||||
<string name="staking_notification_new_validator_funds_transfer">Beim Staking im %1$s -Netzwerk mit einem neuen Validator werden alle zuvor eingesetzten Kryptos automatisch an diesen Validator übertragen</string>
|
||||
<string name="staking_notification_restake_rewards_text">Reinvestiert Deine verdienten Prämien in Deinen Einsatzbetrag und erhöht so den potenziellen Gewinn.</string>
|
||||
<string name="staking_notification_unlock_text">Entsperre dein Geld, um es aus dem Staking-Prozess abzuheben. Das Freischalten nimmt %s.</string>
|
||||
|
|
|
|||
|
|
@ -75,8 +75,8 @@
|
|||
<string name="common_approval">Aprobación</string>
|
||||
<string name="common_approve">Aprobar</string>
|
||||
<string name="common_attention">Atención</string>
|
||||
<string name="common_balance">Soldo: %s</string>
|
||||
<string name="common_balance_title">Soldo</string>
|
||||
<string name="common_balance">Saldo: %s</string>
|
||||
<string name="common_balance_title">Saldo</string>
|
||||
<string name="common_biometric_authentication">autenticación biométrica</string>
|
||||
<string name="common_biometrics">biometría</string>
|
||||
<string name="common_buy">Comprar</string>
|
||||
|
|
@ -103,7 +103,7 @@
|
|||
<string name="common_enable">Activar</string>
|
||||
<string name="common_enabled">Activado</string>
|
||||
<string name="common_error">Error</string>
|
||||
<string name="common_exchange">Intercambie</string>
|
||||
<string name="common_exchange">Intercambiar</string>
|
||||
<string name="common_explore">Explore</string>
|
||||
<string name="common_explore_transaction_history">Explore historial de transacciones</string>
|
||||
<string name="common_explorer">Explorador(a)</string>
|
||||
|
|
@ -211,7 +211,7 @@
|
|||
<string name="details_row_description_flip_to_hide">Gire la pantalla de su dispositivo hacia abajo para ocultar y mostrar rápidamente los saldos</string>
|
||||
<string name="details_row_subtitle_signed_hashes_format">%s hashes</string>
|
||||
<string name="details_row_title_cid">ID de la tarjeta</string>
|
||||
<string name="details_row_title_contact_to_support">Contacta con el equipo de soporte</string>
|
||||
<string name="details_row_title_contact_to_support">Contactar con el equipo de soporte</string>
|
||||
<string name="details_row_title_create_backup">Vincular más tarjetas</string>
|
||||
<string name="details_row_title_currency">Moneda de la aplicación</string>
|
||||
<string name="details_row_title_flip_to_hide">Girar para ocultar saldos</string>
|
||||
|
|
@ -351,7 +351,7 @@
|
|||
<string name="markets_common_my_portfolio">Mi portafolio</string>
|
||||
<string name="markets_common_title">Mercado</string>
|
||||
<string name="markets_generate_addresses_notification">Para generar direcciones para las redes seleccionadas, debe escanear su tarjeta Tangem</string>
|
||||
<string name="markets_hint">Para agregar tokens, abra esta página o toque la barra de búsqueda</string>
|
||||
<string name="markets_hint">Para agregar tokens, abra esta página o pulse sobre la barra de búsqueda</string>
|
||||
<string name="markets_insights_info_description_message">Los datos de este apartado proceden de las siguientes redes: %s</string>
|
||||
<string name="markets_loading_error_title">No se pueden cargar los datos…</string>
|
||||
<string name="markets_loading_no_data_title">Sin datos</string>
|
||||
|
|
@ -363,11 +363,11 @@
|
|||
<string name="markets_search_token_no_result_title">Sin resultado</string>
|
||||
<string name="markets_select_network">Seleccione una red</string>
|
||||
<string name="markets_select_wallet">Seleccione una billetera</string>
|
||||
<string name="markets_selector_interval_1m_title">1 mo</string>
|
||||
<string name="markets_selector_interval_1y_title">1 año</string>
|
||||
<string name="markets_selector_interval_1m_title">1m</string>
|
||||
<string name="markets_selector_interval_1y_title">1a</string>
|
||||
<string name="markets_selector_interval_24h_title">24h</string>
|
||||
<string name="markets_selector_interval_3m_title">3mos</string>
|
||||
<string name="markets_selector_interval_6m_title">6mos</string>
|
||||
<string name="markets_selector_interval_3m_title">3m</string>
|
||||
<string name="markets_selector_interval_6m_title">6m</string>
|
||||
<string name="markets_selector_interval_7d_title">7d</string>
|
||||
<string name="markets_selector_interval_all_title">Todo</string>
|
||||
<string name="markets_sort_by_experienced_buyers_title">Compradores experimentados</string>
|
||||
|
|
@ -413,6 +413,8 @@
|
|||
<string name="markets_token_details_market_rating_description">Posición en la clasificación de criptomonedas entre todas las monedas según la capitalización de mercado</string>
|
||||
<string name="markets_token_details_market_rating_full">Evaluación de mercado</string>
|
||||
<string name="markets_token_details_max_supply">Suministro máximo</string>
|
||||
<string name="markets_token_details_max_supply_description">La cantidad máxima de monedas o tokens que pueden existir para una criptomoneda en particular</string>
|
||||
<string name="markets_token_details_max_supply_full">Suministro máximo</string>
|
||||
<string name="markets_token_details_metrics">Métrica</string>
|
||||
<string name="markets_token_details_official_links">Enlaces oficiales</string>
|
||||
<string name="markets_token_details_price_performance">Rendimiento de precios</string>
|
||||
|
|
|
|||
|
|
@ -413,6 +413,8 @@
|
|||
<string name="markets_token_details_market_rating_description">Position dans le classement des crypto-monnaies entre toutes les pièces en fonction de la capitalisation boursière</string>
|
||||
<string name="markets_token_details_market_rating_full">Évaluation du marché</string>
|
||||
<string name="markets_token_details_max_supply">Approvisionnement maximal</string>
|
||||
<string name="markets_token_details_max_supply_description">Le nombre maximal de pièces ou de jetons pouvant exister pour une crypto-monnaie particulière</string>
|
||||
<string name="markets_token_details_max_supply_full">Approvisionnement maximal</string>
|
||||
<string name="markets_token_details_metrics">Métriques</string>
|
||||
<string name="markets_token_details_official_links">Official links</string>
|
||||
<string name="markets_token_details_price_performance">Performance des prix</string>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,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="other">%d カード</item>
|
||||
</plurals>
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
<string name="common_enable">有効にする</string>
|
||||
<string name="common_enabled">有効</string>
|
||||
<string name="common_error">エラー</string>
|
||||
<string name="common_exchange">交換</string>
|
||||
<string name="common_exchange">スワップ</string>
|
||||
<string name="common_explore">移動する</string>
|
||||
<string name="common_explore_transaction_history">取引履歴を調べる</string>
|
||||
<string name="common_explorer">エクスプローラー</string>
|
||||
|
|
@ -273,7 +273,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>
|
||||
|
|
@ -409,6 +409,8 @@
|
|||
<string name="markets_token_details_market_rating_description">時価総額に基づくすべてのコイン間の暗号資産評価における位置</string>
|
||||
<string name="markets_token_details_market_rating_full">市場格付け</string>
|
||||
<string name="markets_token_details_max_supply">最大供給量</string>
|
||||
<string name="markets_token_details_max_supply_description">特定の暗号資産に存在しうるコインまたはトークンの最大数</string>
|
||||
<string name="markets_token_details_max_supply_full">最大供給量</string>
|
||||
<string name="markets_token_details_metrics">指標</string>
|
||||
<string name="markets_token_details_official_links">公式リンク</string>
|
||||
<string name="markets_token_details_price_performance">値動き</string>
|
||||
|
|
@ -522,7 +524,7 @@
|
|||
<string name="qr_scanner_camera_denied_title">カメラへのアクセスが拒否されました</string>
|
||||
<string name="receive_bottom_sheet_warning_message">%3$sネットワーク上の%1$s ( %2$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>
|
||||
|
|
@ -570,7 +572,7 @@
|
|||
<string name="scan_card_settings_message">カードをスキャンして設定を変更します。変更はスキャンしたカードにのみ影響し、ウォレットに関連付けられている他のカードには影響しません。</string>
|
||||
<string name="scan_card_settings_title">カードを準備してください!</string>
|
||||
<string name="send_additional_field_already_included">入力したアドレスにすでに含まれています</string>
|
||||
<string name="send_alert_fee_too_high_text">手数料額が推奨額の%s倍となっています。カスタム設定が正しいことを再度確認してください。</string>
|
||||
<string name="send_alert_fee_too_high_text">手数料額が推奨額の%s倍となっています。設定を再度確認し、調整してください。</string>
|
||||
<string name="send_alert_fee_too_low_text">推奨手数料を下回る手数料が指定されたため、取引に遅れが生じる可能性があります。続行しますか?</string>
|
||||
<string name="send_alert_transaction_failed_text">理由: %1$s \nコード: %2$s</string>
|
||||
<string name="send_alert_transaction_failed_title">取引は完了していません</string>
|
||||
|
|
@ -605,7 +607,7 @@
|
|||
<string name="send_notification_exceed_balance_title">合計が残高を超えています</string>
|
||||
<string name="send_notification_existential_deposit_text">セキュリティリスクを防ぐため、ブロックチェーン上にアカウントを維持するには、少なくとも%s の残高が必要です。この金額は残高に残り、引き出すことはできません。</string>
|
||||
<string name="send_notification_existential_deposit_title">アカウント維持に必要な最低残高</string>
|
||||
<string name="send_notification_fee_too_high_text">手数料額が推奨額の%s倍となっています。カスタム設定が正しいことを再度確認してください。</string>
|
||||
<string name="send_notification_fee_too_high_text">手数料額が推奨額の%s倍となっています。設定を再度確認し、調整してください。</string>
|
||||
<string name="send_notification_fee_too_high_title">カスタム手数料が高くなっています</string>
|
||||
<string name="send_notification_high_fee_text">%1$sネットワークの特殊性により、残高全体を転送する場合の手数料は高くなります。手数料を削減するには、 %2$sを残します。</string>
|
||||
<string name="send_notification_high_fee_title">手数料が高くなっています</string>
|
||||
|
|
@ -690,6 +692,7 @@
|
|||
<string name="staking_notification_earn_rewards_text_period_week">ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。</string>
|
||||
<string name="staking_notification_earn_rewards_text_weekly">安全にステーキングして、報酬を毎週獲得しましょう</string>
|
||||
<string name="staking_notification_earn_rewards_title">ステーキング報酬を獲得</string>
|
||||
<string name="staking_notification_network_error_text">ネットワークの状態により、現在ステーキングはご利用いただけません。しばらくしてからもう一度お試しください。</string>
|
||||
<string name="staking_notification_new_validator_funds_transfer">新しいバリデーターで%1$sネットワークにステーキングすると、以前にステーキングされた資金はすべてこのバリデーターに自動的に転送されます。</string>
|
||||
<string name="staking_notification_restake_rewards_text">獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。</string>
|
||||
<string name="staking_notification_unlock_text">資金をステーキングから引き出すには、ロックを解除してください。ロック解除には%sかかります。</string>
|
||||
|
|
@ -706,14 +709,14 @@
|
|||
<string name="staking_revote">再投票</string>
|
||||
<string name="staking_reward_claiming_auto">自動</string>
|
||||
<string name="staking_reward_claiming_manual">手動</string>
|
||||
<string name="staking_reward_schedule_block">ブロック</string>
|
||||
<string name="staking_reward_schedule_day">日</string>
|
||||
<string name="staking_reward_schedule_block">ブロック毎</string>
|
||||
<string name="staking_reward_schedule_day">毎日</string>
|
||||
<string name="staking_reward_schedule_each_day">毎日</string>
|
||||
<string name="staking_reward_schedule_epoch">エポック</string>
|
||||
<string name="staking_reward_schedule_era">時代</string>
|
||||
<string name="staking_reward_schedule_hour">時</string>
|
||||
<string name="staking_reward_schedule_month">月</string>
|
||||
<string name="staking_reward_schedule_week">週</string>
|
||||
<string name="staking_reward_schedule_epoch">エポック毎</string>
|
||||
<string name="staking_reward_schedule_era">時代毎</string>
|
||||
<string name="staking_reward_schedule_hour">毎時間</string>
|
||||
<string name="staking_reward_schedule_month">毎月</string>
|
||||
<string name="staking_reward_schedule_week">毎週</string>
|
||||
<string name="staking_rewards">報酬</string>
|
||||
<string name="staking_stake_locked">ステーキングはロックされています</string>
|
||||
<string name="staking_stake_more">もっとステーキングする</string>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<string name="alert_manage_tokens_unsupported_message">Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки.</string>
|
||||
<string name="alert_troubleshooting_scan_card_title">У вас возникли трудности со сканированием карты?</string>
|
||||
<string name="alert_unsupported_card">Эта карта не предназначена для работы с этим приложением</string>
|
||||
<string name="app_settings_default_fee">Комиссия по-умолчанию</string>
|
||||
<string name="app_settings_default_fee_footer">Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться.</string>
|
||||
<string name="app_settings_enable_biometrics_description">Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem</string>
|
||||
<string name="app_settings_enable_biometrics_title">Включите биометрическую аутентификацию</string>
|
||||
|
|
@ -178,6 +179,7 @@
|
|||
<string name="custom_token_contract_address_input_title">Адрес контракта</string>
|
||||
<string name="custom_token_creation_error_invalid_contract_address">Адрес контракта некорректен</string>
|
||||
<string name="custom_token_creation_error_network_not_selected">Пожалуйста, выберите сеть</string>
|
||||
<string name="custom_token_creation_error_token_already_exist_message">Этот токен уже добавлен в список</string>
|
||||
<string name="custom_token_creation_error_token_already_exist_title">Токен уже существует</string>
|
||||
<string name="custom_token_creation_error_wrong_decimals">Десятичное число должно быть действительным целым числом, до %d</string>
|
||||
<string name="custom_token_custom_derivation">Своя деривация</string>
|
||||
|
|
@ -347,6 +349,7 @@
|
|||
<string name="manage_tokens_wallet_support_only_one_network_title">Кошелёк не поддерживает более одной сети</string>
|
||||
<string name="markets_add_to_my_portfolio_description">Чтобы купить, обменять или получить данный токен, вам нужно добавить его к себе в портфель</string>
|
||||
<string name="markets_add_to_my_portfolio_unavailable_description">Этот актив недоступен</string>
|
||||
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">Этот токен не доступен для данного кошелька</string>
|
||||
<string name="markets_add_to_portfolio_button">Добавить в портфель</string>
|
||||
<string name="markets_add_token">Добавить</string>
|
||||
<string name="markets_available_networks">Доступные сети</string>
|
||||
|
|
@ -358,7 +361,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_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>
|
||||
|
|
@ -416,7 +419,9 @@
|
|||
<string name="markets_token_details_market_rating">Рейтинг</string>
|
||||
<string name="markets_token_details_market_rating_description">Позиция в рейтинге криптовалют среди всех монет на основе рыночной капитализации.</string>
|
||||
<string name="markets_token_details_market_rating_full">Рейтинг</string>
|
||||
<string name="markets_token_details_max_supply">Максимальный объем</string>
|
||||
<string name="markets_token_details_max_supply">Макс. объем</string>
|
||||
<string name="markets_token_details_max_supply_description">Максимальное количество монет или токенов, которое может когда-либо существовать для выбранной криптовалюты</string>
|
||||
<string name="markets_token_details_max_supply_full">Максимальный объем</string>
|
||||
<string name="markets_token_details_metrics">Метрики</string>
|
||||
<string name="markets_token_details_official_links">Официальные ссылки</string>
|
||||
<string name="markets_token_details_price_performance">Динамика цены</string>
|
||||
|
|
@ -424,7 +429,7 @@
|
|||
<string name="markets_token_details_security_score">Оценка безопасности</string>
|
||||
<string name="markets_token_details_social">Социальные</string>
|
||||
<string name="markets_token_details_total_supply">Общ. предл.</string>
|
||||
<string name="markets_token_details_total_supply_description">Максимальное количество монет или токенов, которое может когда-либо существовать для выбранной криптовалюты.</string>
|
||||
<string name="markets_token_details_total_supply_description">Максимальное количество монет или токенов, которое может когда-либо существовать для выбранной криптовалюты</string>
|
||||
<string name="markets_token_details_total_supply_full">Общее предложение</string>
|
||||
<string name="markets_token_details_trading_volume">Объем торгов (24ч)</string>
|
||||
<string name="markets_token_details_trading_volume_24h_description">Общая сумма криптовалюты, которая была продана за последние 24 часа, показывающая уровень активности и ликвидности на рынке.</string>
|
||||
|
|
@ -536,7 +541,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">Переводите средства с другого кошелька или биржи</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>
|
||||
|
|
@ -597,6 +602,7 @@
|
|||
<string name="send_amount_label">Сумма</string>
|
||||
<string name="send_bitcoin_custom_fee_footer">Вы можете установить комиссию за транзакцию, изменив значение в поле Satoshi per vByte.</string>
|
||||
<string name="send_custom_amount_fee_footer">Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение.</string>
|
||||
<string name="send_custom_evm_max_fee">Максимальная комиссия</string>
|
||||
<string name="send_custom_evm_max_fee_footer">Это стоимость, которую вы готовы заплатить за каждую единицу газа. Чем выше цена газа, тем быстрее ваша транзакция будет обработана. (Приоритетная комиссия включена)</string>
|
||||
<string name="send_custom_evm_priority_fee">Приоритетная комиссия</string>
|
||||
<string name="send_custom_evm_priority_fee_footer">Комиссия, которую пользователь может заплатить майнерам или валидаторам за ускорение включения его транзакции в блок.</string>
|
||||
|
|
@ -685,7 +691,7 @@
|
|||
<string name="staking_details_minimum_requirement">Минимальное количество</string>
|
||||
<string name="staking_details_no_rewards_to_claim">Нет вознаграждений к получению</string>
|
||||
<string name="staking_details_reward_claiming">Способ вознаграждения</string>
|
||||
<string name="staking_details_reward_claiming_info">Способ получения вознаграждений за стейкинг. Он может быть автоматическим, при котором вознаграждение само зачисляется вам на адрес или в ручную, когда вознаграждение нужно вывести, создав транзакцию на её получение.</string>
|
||||
<string name="staking_details_reward_claiming_info">Способ получения вознаграждений за стейкинг. Оно может зачисляться автоматически на ваш адрес или вручную, когда вознаграждение нужно вывести, создав транзакцию на её получение.</string>
|
||||
<string name="staking_details_reward_schedule">Период вознагражения</string>
|
||||
<string name="staking_details_reward_schedule_info">Это период, определяющий, когда участники стейкинга получат свои вознаграждения.</string>
|
||||
<string name="staking_details_rewards_to_claim">Вознаграждение для получения: %s</string>
|
||||
|
|
@ -725,14 +731,14 @@
|
|||
<string name="staking_revote">Переголосовать</string>
|
||||
<string name="staking_reward_claiming_auto">Авто</string>
|
||||
<string name="staking_reward_claiming_manual">Вручную</string>
|
||||
<string name="staking_reward_schedule_block">Блок</string>
|
||||
<string name="staking_reward_schedule_day">День</string>
|
||||
<string name="staking_reward_schedule_block">За блок</string>
|
||||
<string name="staking_reward_schedule_day">Ежедневно</string>
|
||||
<string name="staking_reward_schedule_each_day">Каждый день</string>
|
||||
<string name="staking_reward_schedule_epoch">Эпоха</string>
|
||||
<string name="staking_reward_schedule_era">Эра</string>
|
||||
<string name="staking_reward_schedule_hour">Час</string>
|
||||
<string name="staking_reward_schedule_epoch">За эпоху</string>
|
||||
<string name="staking_reward_schedule_era">За эру</string>
|
||||
<string name="staking_reward_schedule_hour">Ежечасно</string>
|
||||
<string name="staking_reward_schedule_month">Месяц</string>
|
||||
<string name="staking_reward_schedule_week">Неделя</string>
|
||||
<string name="staking_reward_schedule_week">Еженедельно</string>
|
||||
<string name="staking_rewards">Вознаграждения</string>
|
||||
<string name="staking_stake_locked">Стейкинг закрыт</string>
|
||||
<string name="staking_stake_more">Застейкать еще</string>
|
||||
|
|
@ -815,6 +821,7 @@
|
|||
<string name="transaction_history_operation">Операция</string>
|
||||
<string name="transaction_history_transaction_from_address">от: %s</string>
|
||||
<string name="transaction_history_transaction_to_address">на: %s</string>
|
||||
<string name="transaction_history_transaction_validator">валидатор: %s</string>
|
||||
<string name="try_to_load_data_again_button_title">Попробовать снова</string>
|
||||
<string name="twin_error_same_card">Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d</string>
|
||||
<string name="twin_error_wrong_twin">Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую</string>
|
||||
|
|
|
|||
|
|
@ -418,7 +418,8 @@
|
|||
<string name="markets_token_details_market_rating">Рейтинг</string>
|
||||
<string name="markets_token_details_market_rating_description">Позиція в крипторейтингу між усіма монетами на основі ринкової капіталізації</string>
|
||||
<string name="markets_token_details_market_rating_full">Рейтинг</string>
|
||||
<string name="markets_token_details_max_supply">Максимальна пропозиція</string>
|
||||
<string name="markets_token_details_max_supply_description">Максимальна кількість монет або токенів, яка може коли-небудь існувати для певної криптовалюти</string>
|
||||
<string name="markets_token_details_max_supply_full">Максимальна пропозиція</string>
|
||||
<string name="markets_token_details_metrics">Метрики</string>
|
||||
<string name="markets_token_details_official_links">Офіційні посилання</string>
|
||||
<string name="markets_token_details_price_performance">Цінова ефективність</string>
|
||||
|
|
|
|||
|
|
@ -345,6 +345,7 @@
|
|||
<string name="manage_tokens_wallet_support_only_one_network_title">The wallet doesn\'t support more than one network</string>
|
||||
<string name="markets_add_to_my_portfolio_description">To buy, exchange, or receive this asset, add it to your portfolio</string>
|
||||
<string name="markets_add_to_my_portfolio_unavailable_description">This asset is not available</string>
|
||||
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">This asset is not available for this wallet</string>
|
||||
<string name="markets_add_to_portfolio_button">Add to portfolio</string>
|
||||
<string name="markets_add_token">Add</string>
|
||||
<string name="markets_available_networks">Available networks</string>
|
||||
|
|
@ -413,6 +414,8 @@
|
|||
<string name="markets_token_details_market_rating_description">Position in crypto rating between all coins based on market capitalization</string>
|
||||
<string name="markets_token_details_market_rating_full">Market rating</string>
|
||||
<string name="markets_token_details_max_supply">Max supply</string>
|
||||
<string name="markets_token_details_max_supply_description">The maximum number of coins or tokens that can ever exist for a particular cryptocurrency</string>
|
||||
<string name="markets_token_details_max_supply_full">Max supply</string>
|
||||
<string name="markets_token_details_metrics">Metrics</string>
|
||||
<string name="markets_token_details_official_links">Official links</string>
|
||||
<string name="markets_token_details_price_performance">Price performance</string>
|
||||
|
|
@ -528,7 +531,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">Transfer funds from another wallet or exchange</string>
|
||||
<string name="receive_token_description">Transfer funds from any 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>
|
||||
|
|
@ -715,14 +718,14 @@
|
|||
<string name="staking_revote">Revote</string>
|
||||
<string name="staking_reward_claiming_auto">Auto</string>
|
||||
<string name="staking_reward_claiming_manual">Manual</string>
|
||||
<string name="staking_reward_schedule_block">Block</string>
|
||||
<string name="staking_reward_schedule_day">Day</string>
|
||||
<string name="staking_reward_schedule_block">Per Block</string>
|
||||
<string name="staking_reward_schedule_day">Daily</string>
|
||||
<string name="staking_reward_schedule_each_day">Daily</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>
|
||||
<string name="staking_reward_schedule_month">Month</string>
|
||||
<string name="staking_reward_schedule_week">Week</string>
|
||||
<string name="staking_reward_schedule_epoch">Per Epoch</string>
|
||||
<string name="staking_reward_schedule_era">Per Era</string>
|
||||
<string name="staking_reward_schedule_hour">Hourly</string>
|
||||
<string name="staking_reward_schedule_month">Monthly</string>
|
||||
<string name="staking_reward_schedule_week">Weekly</string>
|
||||
<string name="staking_rewards">Rewards</string>
|
||||
<string name="staking_stake_locked">Stake locked</string>
|
||||
<string name="staking_stake_more">Stake more</string>
|
||||
|
|
@ -805,6 +808,7 @@
|
|||
<string name="transaction_history_operation">Operation</string>
|
||||
<string name="transaction_history_transaction_from_address">from: %s</string>
|
||||
<string name="transaction_history_transaction_to_address">to: %s</string>
|
||||
<string name="transaction_history_transaction_validator">validator: %s</string>
|
||||
<string name="try_to_load_data_again_button_title">Try again</string>
|
||||
<string name="twin_error_same_card">You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d</string>
|
||||
<string name="twin_error_wrong_twin">You\'ve scanned wrong twin card. Please try another one</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
@Composable
|
||||
fun TopAppBarButton(button: TopAppBarButtonUM, tint: Color, modifier: Modifier = Modifier) {
|
||||
IconButton(
|
||||
enabled = button.enabled,
|
||||
modifier = modifier.size(TangemTheme.dimens.size32),
|
||||
onClick = button.onIconClicked,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -6,14 +6,18 @@ import com.tangem.core.ui.R
|
|||
data class TopAppBarButtonUM(
|
||||
@DrawableRes val iconRes: Int,
|
||||
val onIconClicked: () -> Unit,
|
||||
val enabled: Boolean = true,
|
||||
) {
|
||||
|
||||
@Suppress("FunctionName")
|
||||
companion object {
|
||||
|
||||
fun Back(onBackClicked: () -> Unit) = TopAppBarButtonUM(
|
||||
fun Back(onBackClicked: () -> Unit) = Back(true, onBackClicked)
|
||||
|
||||
fun Back(enabled: Boolean = true, onBackClicked: () -> Unit) = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
onIconClicked = onBackClicked,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -207,8 +207,8 @@ class TangemSheetState(
|
|||
absVelocityThreshold = 0.5f,
|
||||
),
|
||||
confirmValueChange = confirmValueChange,
|
||||
positionalThreshold = { with(density) { 56.dp.toPx() } },
|
||||
velocityThreshold = { with(density) { 125.dp.toPx() } },
|
||||
positionalThreshold = { with(density) { 156.dp.toPx() } },
|
||||
velocityThreshold = { with(density) { 225.dp.toPx() } },
|
||||
)
|
||||
|
||||
internal val offset: Float
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ 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.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.constraintlayout.compose.ChainStyle
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import androidx.constraintlayout.compose.Visibility
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
|
|
@ -68,6 +70,9 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod
|
|||
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
|
||||
val (iconItem, titleItem, subtitleItem, amountItem, timestampItem) = createRefs()
|
||||
|
||||
createVerticalChain(titleItem, subtitleItem, chainStyle = ChainStyle.Spread)
|
||||
createVerticalChain(amountItem, timestampItem, chainStyle = ChainStyle.Spread)
|
||||
|
||||
Icon(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
|
|
@ -102,6 +107,7 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod
|
|||
end = TangemTheme.dimens.spacing4,
|
||||
)
|
||||
.constrainAs(subtitleItem) {
|
||||
visibility = state.isGoneIf { subtitle.isNullOrEmpty() }
|
||||
top.linkTo(titleItem.bottom)
|
||||
bottom.linkTo(parent.bottom)
|
||||
start.linkTo(iconItem.end)
|
||||
|
|
@ -114,6 +120,7 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod
|
|||
state = state,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier.constrainAs(amountItem) {
|
||||
visibility = state.isGoneIf { amount.isEmpty() }
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(timestampItem.top)
|
||||
start.linkTo(titleItem.end)
|
||||
|
|
@ -315,6 +322,10 @@ private fun LockedContent(modifier: Modifier = Modifier) {
|
|||
)
|
||||
}
|
||||
|
||||
private fun TransactionState.isGoneIf(goneCondition: TransactionState.Content.() -> Boolean): Visibility {
|
||||
return if ((this as? TransactionState.Content)?.goneCondition() == true) Visibility.Gone else Visibility.Visible
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 368)
|
||||
@Preview(showBackground = true, widthDp = 368, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ fun TangemTheme(
|
|||
dimens: TangemDimens = TangemTheme.dimens,
|
||||
vibratorHapticManager: VibratorHapticManager? = null,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
overrideSystemBarColors: Boolean = true,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val themeColors = if (isDark) darkThemeColors() else lightThemeColors()
|
||||
|
|
@ -32,14 +33,18 @@ fun TangemTheme(
|
|||
.also { it.update(themeColors) }
|
||||
|
||||
val shapes = remember { TangemShapes(dimens) }
|
||||
val systemUiController = rememberSystemUiController()
|
||||
|
||||
SideEffect {
|
||||
systemUiController.setSystemBarsColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = !isDark,
|
||||
isNavigationBarContrastEnforced = false,
|
||||
)
|
||||
// we don't want to override system bar colors in case of fragment bottom sheets for example
|
||||
if (overrideSystemBarColors) {
|
||||
val systemUiController = rememberSystemUiController()
|
||||
|
||||
SideEffect {
|
||||
systemUiController.setSystemBarsColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = !isDark,
|
||||
isNavigationBarContrastEnforced = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
|
|
|
|||
|
|
@ -52,7 +52,11 @@ abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), Compose
|
|||
override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return createComposeView(inflater.context, requireActivity())
|
||||
return createComposeView(
|
||||
context = inflater.context,
|
||||
activity = requireActivity(),
|
||||
overrideSystemBarColors = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
|
|
|||
|
|
@ -50,9 +50,14 @@ internal interface ComposeScreen {
|
|||
* Creates a [ComposeView] with the defined content for the Compose screen.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param overrideSystemBarColors Whether to override the system bar colors to make them transparent.
|
||||
* @return A [ComposeView] instance with the defined screen content.
|
||||
*/
|
||||
internal fun ComposeScreen.createComposeView(context: Context, activity: Activity): ComposeView {
|
||||
internal fun ComposeScreen.createComposeView(
|
||||
context: Context,
|
||||
activity: Activity,
|
||||
overrideSystemBarColors: Boolean = true,
|
||||
): ComposeView {
|
||||
return ComposeView(context).apply {
|
||||
setContent {
|
||||
val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode
|
||||
|
|
@ -63,6 +68,7 @@ internal fun ComposeScreen.createComposeView(context: Context, activity: Activit
|
|||
windowSize = windowSize,
|
||||
vibratorHapticManager = uiDependencies.vibratorHapticManager,
|
||||
snackbarHostState = uiDependencies.globalSnackbarHostState,
|
||||
overrideSystemBarColors = overrideSystemBarColors,
|
||||
) {
|
||||
ScreenContent(modifier = screenModifier)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency
|
|||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import com.tangem.utils.StringsSigns.LOWER_SIGN
|
||||
import com.tangem.utils.StringsSigns.TILDE_SIGN
|
||||
import com.tangem.utils.extensions.isNotWhitespace
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
|
@ -20,8 +21,6 @@ object BigDecimalFormatter {
|
|||
private const val CAN_BE_LOWER_SIGN = LOWER_SIGN
|
||||
private val FORMAT_THRESHOLD = BigDecimal("0.01")
|
||||
|
||||
private const val TEMP_CURRENCY_CODE = "USD"
|
||||
|
||||
private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01")
|
||||
private val CRYPTO_FEE_FORMAT_THRESHOLD = BigDecimal("0.000001")
|
||||
|
||||
|
|
@ -29,6 +28,12 @@ object BigDecimalFormatter {
|
|||
private const val FIAT_MARKET_EXTENDED_DIGITS = 6
|
||||
private const val FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES = 4
|
||||
|
||||
private val usdCurrency = Currency.getInstance("USD")
|
||||
|
||||
@Deprecated(
|
||||
"Use formatCryptoAmount2",
|
||||
replaceWith = ReplaceWith("formatCryptoAmount2"),
|
||||
)
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
|
|
@ -53,6 +58,30 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
// Migrate to this method from formatCryptoAmount ([REDACTED_TASK_KEY])
|
||||
fun formatCryptoAmount2(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = usdCurrency
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||
minimumFractionDigits = 2
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount)
|
||||
.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.symbol,
|
||||
cryptoCurrencySymbol = cryptoCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
fun formatCryptoAmountShorted(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: String,
|
||||
|
|
@ -305,7 +334,7 @@ object BigDecimalFormatter {
|
|||
.getOrElse { e ->
|
||||
// Currency code is not valid ISO 4217 code
|
||||
if (e is IllegalArgumentException) {
|
||||
Currency.getInstance(TEMP_CURRENCY_CODE)
|
||||
usdCurrency
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
|
|
@ -316,7 +345,7 @@ object BigDecimalFormatter {
|
|||
* Adds a proper currency sign for the provided formatted [amount]
|
||||
* ex. '10.0k" -> "$10.0k", "string" -> "$string"
|
||||
*/
|
||||
fun addCurrencySymbolToStringAmount(
|
||||
private fun addCurrencySymbolToStringAmount(
|
||||
amount: String,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
|
|
@ -338,6 +367,32 @@ object BigDecimalFormatter {
|
|||
return formatted
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a proper currency sign for the provided formatted [amount]
|
||||
* ex. '10.0k" -> "ETH 10.0k", "string" -> "ETH string"
|
||||
*/
|
||||
private fun addCryptoCurrencySymbolToStringAmount(
|
||||
amount: String,
|
||||
cryptoCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val sampleAmount = BigDecimal.TEN
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
maximumFractionDigits = 0
|
||||
minimumFractionDigits = 0
|
||||
currency = usdCurrency
|
||||
}
|
||||
|
||||
val formatted = formatter.format(sampleAmount)
|
||||
.replace(sampleAmount.toString(), amount)
|
||||
|
||||
return formatted.replaceFiatSymbolWithCrypto(
|
||||
fiatCurrencySymbol = usdCurrency.symbol,
|
||||
cryptoCurrencySymbol = cryptoCurrencySymbol,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* "123456.6" -> "$123.457K"
|
||||
* "12345.6" -> "$123.046K"
|
||||
|
|
@ -380,6 +435,45 @@ object BigDecimalFormatter {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* "123456.6" -> "ETH 123.457K"
|
||||
* "12345.6" -> "123.046K ETH"
|
||||
* Negative amount is not supported
|
||||
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
|
||||
* @param scale the number of digits to the right of the decimal point
|
||||
*/
|
||||
fun formatCompactCryptoAmount(
|
||||
amount: BigDecimal?,
|
||||
cryptoCurrencySymbol: String,
|
||||
threeDigitsMethod: Boolean = false,
|
||||
decimals: Int = 0,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (amount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
if (amount < BigDecimal.ONE) {
|
||||
return formatCryptoAmount2(
|
||||
cryptoAmount = amount,
|
||||
cryptoCurrency = cryptoCurrencySymbol,
|
||||
decimals = decimals,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
val rawAmount = formatCompactAmount(
|
||||
amount = amount,
|
||||
locale = locale,
|
||||
threeDigitsMethod = threeDigitsMethod,
|
||||
scale = decimals,
|
||||
)
|
||||
|
||||
return addCryptoCurrencySymbolToStringAmount(
|
||||
amount = rawAmount,
|
||||
cryptoCurrencySymbol = cryptoCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* "123456.6" -> "123.457K"
|
||||
* "12345.6" -> "123.046K"
|
||||
|
|
@ -413,14 +507,61 @@ object BigDecimalFormatter {
|
|||
|
||||
return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP))
|
||||
} else {
|
||||
val value = amount.setScale(scale, RoundingMode.HALF_UP)
|
||||
val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP)
|
||||
val digitsCount = scaledAmount.longValueExact().toString().count()
|
||||
val digitsToFormat = 5 - when (digitsCount % 3) {
|
||||
0 -> 0
|
||||
1 -> 2
|
||||
else -> 1
|
||||
}
|
||||
|
||||
val formatter = CompactDecimalFormat.getInstance(
|
||||
locale,
|
||||
CompactDecimalFormat.CompactStyle.SHORT,
|
||||
)
|
||||
).apply {
|
||||
minimumSignificantDigits = 2
|
||||
maximumSignificantDigits = digitsToFormat
|
||||
}
|
||||
|
||||
return formatter.format(value)
|
||||
return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP))
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces fiat currency symbol with crypto currency symbol
|
||||
// with respect to the position of the symbol and whitespace
|
||||
private fun String.replaceFiatSymbolWithCrypto(fiatCurrencySymbol: String, cryptoCurrencySymbol: String): String {
|
||||
val str = this
|
||||
if (str.isEmpty()) return str
|
||||
|
||||
return buildString {
|
||||
when {
|
||||
str.endsWith(fiatCurrencySymbol) -> {
|
||||
val withoutSymbol = str.dropLast(fiatCurrencySymbol.length)
|
||||
val last = withoutSymbol.lastOrNull() ?: return cryptoCurrencySymbol
|
||||
|
||||
append(withoutSymbol)
|
||||
|
||||
if (last.isNotWhitespace()) {
|
||||
append("\u2009")
|
||||
}
|
||||
|
||||
append(cryptoCurrencySymbol)
|
||||
}
|
||||
str.startsWith(fiatCurrencySymbol) -> {
|
||||
append(cryptoCurrencySymbol)
|
||||
|
||||
val withoutSymbol = str.drop(fiatCurrencySymbol.length)
|
||||
val first = withoutSymbol.firstOrNull()
|
||||
?: return cryptoCurrencySymbol
|
||||
|
||||
if (first.isNotWhitespace()) {
|
||||
append("\u2009")
|
||||
}
|
||||
|
||||
append(withoutSymbol)
|
||||
}
|
||||
else -> append(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.util.Locale
|
||||
|
||||
internal object BigDecimalFormatterCompat {
|
||||
|
||||
/**
|
||||
* Formats value as [BigDecimalFormatter.formatCompactFiatAmount] does using only "T","B","M","K" suffixes
|
||||
* Used for < API24 compatibility
|
||||
*/
|
||||
@Suppress("MagicNumber", "UnnecessaryParentheses")
|
||||
fun formatCompactFiatAmountNoLocaleContext(
|
||||
amount: BigDecimal,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val formatted = formatCompactAmountNoLocaleContext(amount)
|
||||
|
||||
return BigDecimalFormatter.addCurrencySymbolToStringAmount(
|
||||
amount = formatted,
|
||||
fiatCurrencyCode = fiatCurrencyCode,
|
||||
fiatCurrencySymbol = fiatCurrencySymbol,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes
|
||||
* Used for < API24 compatibility
|
||||
*/
|
||||
@Suppress("MagicNumber", "UnnecessaryParentheses")
|
||||
fun formatCompactAmountNoLocaleContext(amount: BigDecimal): String {
|
||||
val value = amount.setScale(0, RoundingMode.HALF_UP).longValueExact()
|
||||
|
||||
val formatted = when {
|
||||
value > 1_000_000_000_000L -> {
|
||||
val trillion = value / 1_000_000_000_000
|
||||
val billion = (value % 1_000_000_000_000) / 1_000_000_000
|
||||
"$trillion.${billion}T"
|
||||
}
|
||||
value > 1_000_000_000L -> {
|
||||
val billion = value / 1_000_000_000
|
||||
val million = (value % 1_000_000_000) / 1_000_000
|
||||
"$billion.${million}B"
|
||||
}
|
||||
value > 1_000_000L -> {
|
||||
val million = value / 1_000_000
|
||||
val thousand = (value % 1_000_000) / 1_000
|
||||
"$million.${thousand}M"
|
||||
}
|
||||
value > 1_000L -> {
|
||||
val thousand = value / 1_000
|
||||
"${thousand}K"
|
||||
}
|
||||
else -> return value.toString()
|
||||
}
|
||||
|
||||
return formatted
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M5.833,12.5H7.5C7.5,13.4 8.642,14.167 10,14.167C11.358,14.167 12.5,13.4 12.5,12.5C12.5,11.583 11.633,11.25 9.8,10.808C8.033,10.367 5.833,9.817 5.833,7.5C5.833,6.008 7.058,4.742 8.75,4.317V2.5H11.25V4.317C12.942,4.742 14.167,6.008 14.167,7.5H12.5C12.5,6.6 11.358,5.833 10,5.833C8.642,5.833 7.5,6.6 7.5,7.5C7.5,8.417 8.367,8.75 10.2,9.192C11.967,9.633 14.167,10.183 14.167,12.5C14.167,13.992 12.942,15.258 11.25,15.683V17.5H8.75V15.683C7.058,15.258 5.833,13.992 5.833,12.5Z" />
|
||||
|
||||
</vector>
|
||||
|
|
@ -8,5 +8,6 @@ object StringsSigns {
|
|||
const val DASH_SIGN = "—"
|
||||
const val LOWER_SIGN = "<"
|
||||
const val TILDE_SIGN = "~"
|
||||
const val INFINITY_SIGN = "∞"
|
||||
const val NON_BREAKING_SPACE = '\u00A0'
|
||||
}
|
||||
|
|
@ -99,7 +99,7 @@ internal object TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, To
|
|||
circulatingSupply = circulatingSupply,
|
||||
marketCap = marketCap,
|
||||
volume24h = volume24h,
|
||||
totalSupply = totalSupply,
|
||||
maxSupply = maxSupply,
|
||||
fullyDilutedValuation = fullyDilutedValuation,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,22 +45,25 @@ internal class SdkTransactionHistoryItemConverter(
|
|||
private fun SdkTransactionHistoryItem.AddressType.toDomain(): TxHistoryItem.AddressType = when (this) {
|
||||
is SdkTransactionHistoryItem.AddressType.Contract -> TxHistoryItem.AddressType.Contract(address)
|
||||
is SdkTransactionHistoryItem.AddressType.User -> TxHistoryItem.AddressType.User(address)
|
||||
is SdkTransactionHistoryItem.AddressType.Validator -> TxHistoryItem.AddressType.Validator(address)
|
||||
}
|
||||
|
||||
private fun SdkTransactionHistoryItem.extractInteractionAddressType(): TxHistoryItem.InteractionAddressType {
|
||||
return when (type) {
|
||||
private fun SdkTransactionHistoryItem.extractInteractionAddressType(): TxHistoryItem.InteractionAddressType? {
|
||||
return when (val transactionType = type) {
|
||||
SdkTransactionHistoryItem.TransactionType.Transfer -> if (isOutgoing) {
|
||||
mapToInteractionAddressType(destinationType = destinationType)
|
||||
} else {
|
||||
mapToInteractionAddressType(sourceType = sourceType)
|
||||
}
|
||||
is SdkTransactionHistoryItem.TransactionType.TronStakingTransactionType -> {
|
||||
TxHistoryItem.InteractionAddressType.Staking
|
||||
}
|
||||
|
||||
is SdkTransactionHistoryItem.TransactionType.ContractMethod,
|
||||
is SdkTransactionHistoryItem.TransactionType.ContractMethodName,
|
||||
-> mapToInteractionAddressType(destinationType)
|
||||
-> mapToInteractionAddressType(destinationType = destinationType)
|
||||
|
||||
is SdkTransactionHistoryItem.TransactionType.TronStakingTransactionType.VoteWitnessContract -> {
|
||||
TxHistoryItem.InteractionAddressType.Validator(address = transactionType.validatorAddress)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,6 +81,9 @@ internal class SdkTransactionHistoryItemConverter(
|
|||
is TransactionHistoryItem.AddressType.User -> TxHistoryItem.InteractionAddressType.User(
|
||||
destinationType.addressType.address,
|
||||
)
|
||||
is TransactionHistoryItem.AddressType.Validator -> TxHistoryItem.InteractionAddressType.Validator(
|
||||
destinationType.addressType.address,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +95,9 @@ internal class SdkTransactionHistoryItemConverter(
|
|||
is TransactionHistoryItem.SourceType.Multiple -> TxHistoryItem.InteractionAddressType.Multiple(
|
||||
sourceType.addresses,
|
||||
)
|
||||
is TransactionHistoryItem.SourceType.Single -> TxHistoryItem.InteractionAddressType.User(sourceType.address)
|
||||
is TransactionHistoryItem.SourceType.Single -> {
|
||||
TxHistoryItem.InteractionAddressType.User(sourceType.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,9 +27,12 @@ internal class SdkTransactionTypeConverter(
|
|||
TxHistoryItem.TransactionType.TronStakingTransactionType.Unstake
|
||||
}
|
||||
is TransactionType.TronStakingTransactionType.VoteWitnessContract -> {
|
||||
TxHistoryItem.TransactionType.TronStakingTransactionType.Vote
|
||||
TxHistoryItem.TransactionType.TronStakingTransactionType.Vote(value.validatorAddress)
|
||||
}
|
||||
is TransactionType.TronStakingTransactionType.WithdrawBalanceContract -> {
|
||||
TxHistoryItem.TransactionType.TronStakingTransactionType.ClaimRewards
|
||||
}
|
||||
is TransactionType.TronStakingTransactionType.WithdrawExpireUnfreezeContract -> {
|
||||
TxHistoryItem.TransactionType.TronStakingTransactionType.Withdraw
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class GetSupportedNetworksUseCase(
|
|||
|
||||
ensureNotNull(networks.takeIf { it.isNotEmpty() }) {
|
||||
SupportedBlockchainException.EmptyList
|
||||
}
|
||||
}.sortedBy(Network::name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ data class TokenMarketInfo(
|
|||
val circulatingSupply: BigDecimal?,
|
||||
val marketCap: BigDecimal?,
|
||||
val volume24h: BigDecimal?,
|
||||
val totalSupply: BigDecimal?,
|
||||
val maxSupply: BigDecimal?,
|
||||
val fullyDilutedValuation: BigDecimal?,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,4 +17,27 @@ enum class StakingActionType {
|
|||
REBOND,
|
||||
MIGRATE,
|
||||
UNKNOWN,
|
||||
;
|
||||
|
||||
companion object {
|
||||
val StakingActionType.asAnalyticName
|
||||
get() = when (this) {
|
||||
STAKE -> "Stake"
|
||||
UNSTAKE -> "Unstake"
|
||||
CLAIM_REWARDS -> "Claim Rewards"
|
||||
RESTAKE_REWARDS -> "Restake Rewards"
|
||||
WITHDRAW -> "Withdraw"
|
||||
RESTAKE -> "Restake"
|
||||
CLAIM_UNSTAKED -> "Claim Unstaked"
|
||||
UNLOCK_LOCKED -> "Unlock Locked"
|
||||
STAKE_LOCKED -> "Stake Locked"
|
||||
VOTE -> "Vote"
|
||||
REVOKE -> "Revoke"
|
||||
VOTE_LOCKED -> "Vote Locked"
|
||||
REVOTE -> "Revote"
|
||||
REBOND -> "Rebond"
|
||||
MIGRATE -> "Migrate"
|
||||
UNKNOWN -> "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -151,13 +151,12 @@ internal class CurrenciesStatusesOperations(
|
|||
val currenciesFlow = combine(
|
||||
getQuotes(currenciesIds),
|
||||
getNetworksStatuses(networks),
|
||||
getYieldBalances(nonEmptyCurrencies),
|
||||
) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances ->
|
||||
) { maybeQuotes, maybeNetworksStatuses ->
|
||||
createCurrenciesStatuses(
|
||||
currencies = nonEmptyCurrencies,
|
||||
maybeQuotes = maybeQuotes,
|
||||
maybeNetworkStatuses = maybeNetworksStatuses,
|
||||
maybeYieldBalances = maybeYieldBalances,
|
||||
maybeYieldBalances = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -403,15 +402,6 @@ internal class CurrenciesStatusesOperations(
|
|||
.onEmpty { emit(Error.EmptyNetworksStatuses.left()) }
|
||||
}
|
||||
|
||||
private fun getYieldBalances(cryptoCurrencies: List<CryptoCurrency>): Flow<Either<Error, YieldBalanceList>> {
|
||||
return stakingRepository.getMultiYieldBalanceFlow(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencies = cryptoCurrencies,
|
||||
).map<YieldBalanceList, Either<Error, YieldBalanceList>> { it.right() }
|
||||
.catch { emit(Error.DataError(it).left()) }
|
||||
.onEmpty { emit(Error.EmptyYieldBalances.left()) }
|
||||
}
|
||||
|
||||
private suspend fun getYieldBalancesSync(
|
||||
cryptoCurrencies: List<CryptoCurrency>,
|
||||
): Either<Error.EmptyYieldBalances, YieldBalanceList> {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ data class TxHistoryItem(
|
|||
val isOutgoing: Boolean,
|
||||
val destinationType: DestinationType,
|
||||
val sourceType: SourceType,
|
||||
val interactionAddressType: InteractionAddressType,
|
||||
val interactionAddressType: InteractionAddressType?,
|
||||
val status: TransactionStatus,
|
||||
val type: TransactionType,
|
||||
val amount: BigDecimal,
|
||||
|
|
@ -30,6 +30,7 @@ data class TxHistoryItem(
|
|||
|
||||
data class User(override val address: String) : AddressType()
|
||||
data class Contract(override val address: String) : AddressType()
|
||||
data class Validator(override val address: String) : AddressType()
|
||||
}
|
||||
|
||||
sealed interface TransactionType {
|
||||
|
|
@ -40,10 +41,11 @@ data class TxHistoryItem(
|
|||
data class Operation(val name: String) : TransactionType
|
||||
|
||||
sealed interface TronStakingTransactionType : TransactionType {
|
||||
data object Vote : TronStakingTransactionType
|
||||
data object Withdraw : TronStakingTransactionType
|
||||
data class Vote(val validatorAddress: String) : TronStakingTransactionType
|
||||
data object ClaimRewards : TronStakingTransactionType
|
||||
data object Stake : TronStakingTransactionType
|
||||
data object Unstake : TronStakingTransactionType
|
||||
data object Withdraw : TronStakingTransactionType
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +56,7 @@ data class TxHistoryItem(
|
|||
}
|
||||
|
||||
sealed class InteractionAddressType {
|
||||
data object Staking : InteractionAddressType()
|
||||
data class Validator(val address: String) : InteractionAddressType()
|
||||
data class User(val address: String) : InteractionAddressType()
|
||||
data class Contract(val address: String) : InteractionAddressType()
|
||||
data class Multiple(val addresses: List<String>) : InteractionAddressType()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface AddCustomTokenComponent : ComposableBottomSheetComponent {
|
|||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val source: ManageTokensSource,
|
||||
val onDismiss: () -> Unit,
|
||||
val onCurrencyAdded: () -> Unit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
|
||||
interface ManageTokensComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(val userWalletId: UserWalletId?)
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId?,
|
||||
val source: ManageTokensSource,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, ManageTokensComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.managetokens.component
|
||||
|
||||
enum class ManageTokensSource {
|
||||
STORIES,
|
||||
ONBOARDING,
|
||||
SETTINGS,
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ dependencies {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.analytics)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.manageTokens)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
package com.tangem.features.managetokens.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
|
||||
internal sealed class CustomTokenAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(
|
||||
category = "Manage Tokens / Custom",
|
||||
event = event,
|
||||
params = params,
|
||||
) {
|
||||
|
||||
class ScreenOpened(source: ManageTokensSource) : CustomTokenAnalyticsEvent(
|
||||
event = "Custom Token Screen Opened",
|
||||
params = mapOf(AnalyticsParam.Key.SOURCE to source.name),
|
||||
)
|
||||
|
||||
class CustomTokenWasAdded(
|
||||
currencySymbol: String,
|
||||
derivationPath: String,
|
||||
source: ManageTokensSource,
|
||||
) : CustomTokenAnalyticsEvent(
|
||||
event = "Custom Token Was Added",
|
||||
params = mapOf(
|
||||
AnalyticsParam.Key.TOKEN_PARAM to currencySymbol,
|
||||
AnalyticsParam.Key.DERIVATION to derivationPath,
|
||||
AnalyticsParam.Key.SOURCE to source.name,
|
||||
),
|
||||
)
|
||||
|
||||
class NetworkSelected(networkName: String, source: ManageTokensSource) : CustomTokenAnalyticsEvent(
|
||||
event = "Custom Token Network Selected",
|
||||
params = mapOf(
|
||||
AnalyticsParam.Key.BLOCKCHAIN to networkName,
|
||||
AnalyticsParam.Key.SOURCE to source.name,
|
||||
),
|
||||
)
|
||||
|
||||
class DerivationSelected(derivationName: String, source: ManageTokensSource) : CustomTokenAnalyticsEvent(
|
||||
event = "Custom Token Derivation Selected",
|
||||
params = mapOf(
|
||||
AnalyticsParam.Key.DERIVATION to derivationName,
|
||||
AnalyticsParam.Key.SOURCE to source.name,
|
||||
),
|
||||
)
|
||||
|
||||
class Address(isValid: Boolean, source: ManageTokensSource) : CustomTokenAnalyticsEvent(
|
||||
event = "Custom Token Address",
|
||||
params = mapOf(
|
||||
AnalyticsParam.Key.VALIDATION to AnalyticsParam.Validation.from(isValid),
|
||||
AnalyticsParam.Key.SOURCE to source.name,
|
||||
),
|
||||
)
|
||||
|
||||
class Name(source: ManageTokensSource) : CustomTokenAnalyticsEvent(
|
||||
event = "Custom Token Name",
|
||||
params = mapOf(AnalyticsParam.Key.SOURCE to source.name),
|
||||
)
|
||||
|
||||
class Symbol(source: ManageTokensSource) : CustomTokenAnalyticsEvent(
|
||||
event = "Custom Token Symbol",
|
||||
params = mapOf(AnalyticsParam.Key.SOURCE to source.name),
|
||||
)
|
||||
|
||||
class Decimals(source: ManageTokensSource) : CustomTokenAnalyticsEvent(
|
||||
event = "Custom Token Decimals",
|
||||
params = mapOf(AnalyticsParam.Key.SOURCE to source.name),
|
||||
)
|
||||
|
||||
class ButtonCustomToken(source: ManageTokensSource) : CustomTokenAnalyticsEvent(
|
||||
event = "Button - Custom Token",
|
||||
params = mapOf(AnalyticsParam.Key.SOURCE to source.name),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.managetokens.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
|
||||
internal sealed class ManageTokensAnalyticEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(
|
||||
category = "ManageTokens",
|
||||
event = event,
|
||||
params = params,
|
||||
) {
|
||||
|
||||
class ScreenOpened(source: ManageTokensSource) : ManageTokensAnalyticEvent(
|
||||
event = "Manage Tokens Screen Opened",
|
||||
params = mapOf(AnalyticsParam.Key.SOURCE to source.name),
|
||||
)
|
||||
|
||||
class TokensIsNotFound(query: String, source: ManageTokensSource) : ManageTokensAnalyticEvent(
|
||||
event = "Token Is Not Found",
|
||||
params = mapOf(
|
||||
AnalyticsParam.Key.INPUT to query,
|
||||
AnalyticsParam.Key.SOURCE to source.name,
|
||||
),
|
||||
)
|
||||
|
||||
class TokenSwitcherChanged(
|
||||
tokenSymbol: String,
|
||||
isSelected: Boolean,
|
||||
source: ManageTokensSource,
|
||||
) : ManageTokensAnalyticEvent(
|
||||
event = "Token Switcher Changed",
|
||||
params = mapOf(
|
||||
AnalyticsParam.Key.TOKEN_PARAM to tokenSymbol,
|
||||
AnalyticsParam.Key.STATE to AnalyticsParam.OnOffState.from(isSelected),
|
||||
AnalyticsParam.Key.SOURCE to source.name,
|
||||
),
|
||||
)
|
||||
|
||||
class TokenAdded(
|
||||
tokensCount: Int,
|
||||
source: ManageTokensSource,
|
||||
) : ManageTokensAnalyticEvent(
|
||||
event = "Token Added",
|
||||
params = mapOf(
|
||||
AnalyticsParam.Key.COUNT to tokensCount.toString(),
|
||||
AnalyticsParam.Key.SOURCE to source.name,
|
||||
),
|
||||
)
|
||||
|
||||
// TODO: Will be used later
|
||||
data object ButtonLater : ManageTokensAnalyticEvent(event = "Button - Later")
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ internal interface CustomTokenFormComponent : ComposableContentComponent {
|
|||
val network: SelectedNetwork,
|
||||
val derivationPath: SelectedDerivationPath?,
|
||||
val formValues: CustomTokenFormValues,
|
||||
val source: ManageTokensSource,
|
||||
val onSelectNetworkClick: (CustomTokenFormValues) -> Unit,
|
||||
val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit,
|
||||
val onCurrencyAdded: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
|
|||
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.*
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
|
||||
import com.tangem.features.managetokens.component.AddCustomTokenComponent
|
||||
import com.tangem.features.managetokens.component.CustomTokenFormComponent
|
||||
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
|
||||
|
|
@ -29,6 +31,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
|
|||
@Assisted private val params: AddCustomTokenComponent.Params,
|
||||
private val selectorComponentFactory: CustomTokenSelectorComponent.Factory,
|
||||
private val formComponentFactory: CustomTokenFormComponent.Factory,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : AddCustomTokenComponent, AppComponentContext by context {
|
||||
|
||||
private val navigation = StackNavigation<AddCustomTokenConfig>()
|
||||
|
|
@ -45,6 +48,10 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
|
|||
childFactory = ::contentChild,
|
||||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(CustomTokenAnalyticsEvent.ScreenOpened(params.source))
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
params.onDismiss()
|
||||
}
|
||||
|
|
@ -85,9 +92,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
|
|||
params = CustomTokenSelectorComponent.Params.NetworkSelector(
|
||||
userWalletId = config.userWalletId,
|
||||
selectedNetwork = null,
|
||||
onNetworkSelected = { network ->
|
||||
showForm(network = network)
|
||||
},
|
||||
onNetworkSelected = ::changeSelectedNetwork,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -97,9 +102,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
|
|||
params = CustomTokenSelectorComponent.Params.NetworkSelector(
|
||||
userWalletId = config.userWalletId,
|
||||
selectedNetwork = config.selectedNetwork,
|
||||
onNetworkSelected = { network ->
|
||||
showForm(network = network)
|
||||
},
|
||||
onNetworkSelected = ::changeSelectedNetwork,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -112,9 +115,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
|
|||
"Network is not selected"
|
||||
},
|
||||
selectedDerivationPath = config.selectedDerivationPath,
|
||||
onDerivationPathSelected = { derivationPath ->
|
||||
showForm(derivationPath = derivationPath)
|
||||
},
|
||||
onDerivationPathSelected = ::changeDerivationPath,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -128,6 +129,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
|
|||
},
|
||||
derivationPath = config.selectedDerivationPath,
|
||||
formValues = config.formValues,
|
||||
source = params.source,
|
||||
onSelectNetworkClick = ::showNetworkSelector,
|
||||
onSelectDerivationPathClick = ::showDerivationPathSelector,
|
||||
onCurrencyAdded = ::dismissAndNotify,
|
||||
|
|
@ -136,6 +138,26 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun changeSelectedNetwork(network: SelectedNetwork) {
|
||||
val event = CustomTokenAnalyticsEvent.NetworkSelected(
|
||||
networkName = network.name,
|
||||
source = params.source,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
|
||||
showForm(network = network)
|
||||
}
|
||||
|
||||
private fun changeDerivationPath(derivationPath: SelectedDerivationPath) {
|
||||
val event = CustomTokenAnalyticsEvent.DerivationSelected(
|
||||
derivationName = derivationPath.name,
|
||||
source = params.source,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
|
||||
showForm(derivationPath = derivationPath)
|
||||
}
|
||||
|
||||
private fun showDerivationPathSelector(formValues: CustomTokenFormValues) {
|
||||
val currentConfig = contentStack.value.active.configuration
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.managetokens.ValidateDerivationPathUseCase
|
||||
import com.tangem.domain.managetokens.model.exceptoin.DerivationPathValidationException
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
|
|
@ -111,7 +110,8 @@ internal class DefaultCustomTokenDerivationInputComponent @AssistedInject constr
|
|||
val model = SelectedDerivationPath(
|
||||
id = null,
|
||||
value = Network.DerivationPath.Custom(value),
|
||||
networkName = stringReference(value = value),
|
||||
name = value,
|
||||
isDefault = false,
|
||||
)
|
||||
|
||||
params.onConfirm(model)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import dagger.assisted.AssistedInject
|
|||
|
||||
internal class DefaultManageTokensComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted params: ManageTokensComponent.Params,
|
||||
@Assisted private val params: ManageTokensComponent.Params,
|
||||
private val addCustomTokenComponentFactory: AddCustomTokenComponent.Factory,
|
||||
) : ManageTokensComponent, AppComponentContext by context {
|
||||
|
||||
|
|
@ -61,6 +61,7 @@ internal class DefaultManageTokensComponent @AssistedInject constructor(
|
|||
context = childByContext(componentContext),
|
||||
params = AddCustomTokenComponent.Params(
|
||||
userWalletId = config.userWalletId,
|
||||
source = params.source,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
onCurrencyAdded = model::reloadList,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.managetokens.component.preview
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
|
|
@ -13,6 +14,7 @@ import com.tangem.features.managetokens.impl.R
|
|||
import com.tangem.features.managetokens.ui.CustomTokenFormContent
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
|
||||
internal class PreviewCustomTokenFormComponent(
|
||||
networkName: ClickableFieldUM = PreviewCustomTokenFormComponent.networkName,
|
||||
|
|
@ -48,30 +50,40 @@ internal class PreviewCustomTokenFormComponent(
|
|||
onClick = {},
|
||||
)
|
||||
val tokenForm: CustomTokenFormUM.TokenFormUM = CustomTokenFormUM.TokenFormUM(
|
||||
contractAddress = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_contract_address_input_title),
|
||||
placeholder = stringReference(value = "0x000000000000000000000000000"),
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
),
|
||||
name = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_name_input_title),
|
||||
placeholder = stringReference(value = "E.g. USD Coin"),
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
),
|
||||
symbol = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_token_symbol_input_title),
|
||||
placeholder = stringReference(value = "E.g. USDC"),
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
),
|
||||
decimals = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_decimals_input_title),
|
||||
placeholder = stringReference(value = "8"),
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
),
|
||||
fields = mapOf(
|
||||
CustomTokenFormUM.TokenFormUM.Field.CONTRACT_ADDRESS to TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_contract_address_input_title),
|
||||
placeholder = stringReference(value = "0x000000000000000000000000000"),
|
||||
value = "",
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
onValueChange = {},
|
||||
onFocusChange = {},
|
||||
),
|
||||
CustomTokenFormUM.TokenFormUM.Field.NAME to TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_name_input_title),
|
||||
placeholder = stringReference(value = "E.g. USD Coin"),
|
||||
value = "",
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
onValueChange = {},
|
||||
onFocusChange = {},
|
||||
),
|
||||
CustomTokenFormUM.TokenFormUM.Field.SYMBOL to TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_token_symbol_input_title),
|
||||
placeholder = stringReference(value = "E.g. USDC"),
|
||||
value = "",
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
onValueChange = {},
|
||||
onFocusChange = {},
|
||||
),
|
||||
CustomTokenFormUM.TokenFormUM.Field.DECIMALS to TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_decimals_input_title),
|
||||
placeholder = stringReference(value = "8"),
|
||||
value = "",
|
||||
keyboardOptions = KeyboardOptions(),
|
||||
onValueChange = {},
|
||||
onFocusChange = {},
|
||||
),
|
||||
).toPersistentMap(),
|
||||
)
|
||||
val notifications: PersistentList<CustomTokenFormUM.NotificationUM> = persistentListOf(
|
||||
CustomTokenFormUM.NotificationUM(
|
||||
|
|
|
|||
|
|
@ -31,13 +31,14 @@ internal class PreviewCustomTokenSelectorComponent(
|
|||
val d = SelectedDerivationPath(
|
||||
id = Network.ID(index.toString()),
|
||||
value = Network.DerivationPath.Card("m/44'/0'/0'/0/$index"),
|
||||
networkName = stringReference(value = "Network $index"),
|
||||
name = "Network $index",
|
||||
isDefault = false,
|
||||
)
|
||||
|
||||
DerivationPathUM(
|
||||
id = d.id?.value ?: "",
|
||||
value = d.value.value.orEmpty(),
|
||||
networkName = d.networkName,
|
||||
networkName = stringReference(d.name),
|
||||
isSelected = d.value == params.selectedDerivationPath?.value,
|
||||
onSelectedStateChange = { params.onDerivationPathSelected(d) },
|
||||
)
|
||||
|
|
@ -45,7 +46,7 @@ internal class PreviewCustomTokenSelectorComponent(
|
|||
is Params.NetworkSelector -> {
|
||||
val n = SelectedNetwork(
|
||||
id = Network.ID(index.toString()),
|
||||
name = stringReference(value = "Network $index"),
|
||||
name = "Network $index",
|
||||
derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/$index"),
|
||||
canHandleTokens = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.features.managetokens.entity.customtoken
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
|
@ -27,7 +26,7 @@ internal data class AddCustomTokenConfig(
|
|||
@Serializable
|
||||
internal data class SelectedNetwork(
|
||||
val id: Network.ID,
|
||||
val name: TextReference,
|
||||
val name: String,
|
||||
val derivationPath: Network.DerivationPath,
|
||||
val canHandleTokens: Boolean,
|
||||
)
|
||||
|
|
@ -36,5 +35,6 @@ internal data class SelectedNetwork(
|
|||
internal data class SelectedDerivationPath(
|
||||
val id: Network.ID?,
|
||||
val value: Network.DerivationPath,
|
||||
val networkName: TextReference,
|
||||
val name: String,
|
||||
val isDefault: Boolean,
|
||||
)
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.features.managetokens.entity.customtoken
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.PersistentMap
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal data class CustomTokenFormUM(
|
||||
|
|
@ -16,12 +18,17 @@ internal data class CustomTokenFormUM(
|
|||
) {
|
||||
|
||||
data class TokenFormUM(
|
||||
val contractAddress: TextInputFieldUM,
|
||||
val name: TextInputFieldUM,
|
||||
val symbol: TextInputFieldUM,
|
||||
val decimals: TextInputFieldUM,
|
||||
val fields: PersistentMap<Field, TextInputFieldUM>,
|
||||
val wasFilled: Boolean = false,
|
||||
)
|
||||
) {
|
||||
|
||||
enum class Field {
|
||||
CONTRACT_ADDRESS,
|
||||
NAME,
|
||||
SYMBOL,
|
||||
DECIMALS,
|
||||
}
|
||||
}
|
||||
|
||||
data class NotificationUM(
|
||||
val id: String,
|
||||
|
|
@ -32,10 +39,13 @@ internal data class CustomTokenFormUM(
|
|||
internal data class TextInputFieldUM(
|
||||
val label: TextReference,
|
||||
val placeholder: TextReference,
|
||||
val keyboardOptions: KeyboardOptions,
|
||||
val value: String = "",
|
||||
val isFocused: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
val isEnabled: Boolean = true,
|
||||
val onValueChange: (String) -> Unit,
|
||||
val onFocusChange: (Boolean) -> Unit,
|
||||
)
|
||||
|
||||
internal data class ClickableFieldUM(
|
||||
|
|
|
|||
|
|
@ -1,45 +1,35 @@
|
|||
package com.tangem.features.managetokens.entity.customtoken
|
||||
|
||||
import com.tangem.domain.managetokens.model.AddCustomTokenForm
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM.Field
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@JvmInline
|
||||
@Serializable
|
||||
internal value class CustomTokenFormValues private constructor(private val values: List<String>) {
|
||||
|
||||
constructor() : this(values = emptyList())
|
||||
internal class CustomTokenFormValues(
|
||||
private val contractAddress: String = "",
|
||||
private val name: String = "",
|
||||
private val symbol: String = "",
|
||||
private val decimals: String = "",
|
||||
) {
|
||||
|
||||
constructor(form: TokenFormUM?) : this(
|
||||
values = if (form == null) {
|
||||
emptyList()
|
||||
} else {
|
||||
listOf(
|
||||
form.contractAddress.value,
|
||||
form.name.value,
|
||||
form.symbol.value,
|
||||
form.decimals.value,
|
||||
)
|
||||
},
|
||||
contractAddress = form?.fields?.get(Field.CONTRACT_ADDRESS)?.value.orEmpty(),
|
||||
name = form?.fields?.get(Field.NAME)?.value.orEmpty(),
|
||||
symbol = form?.fields?.get(Field.SYMBOL)?.value.orEmpty(),
|
||||
decimals = form?.fields?.get(Field.DECIMALS)?.value.orEmpty(),
|
||||
)
|
||||
|
||||
fun fillValues(to: TokenFormUM): TokenFormUM = to.copy(
|
||||
contractAddress = to.contractAddress.copy(value = values.getOrElse(index = 0) { "" }),
|
||||
name = to.name.copy(value = values.getOrElse(index = 1) { "" }),
|
||||
symbol = to.symbol.copy(value = values.getOrElse(index = 2) { "" }),
|
||||
decimals = to.decimals.copy(value = values.getOrElse(index = 3) { "" }),
|
||||
)
|
||||
|
||||
fun toDomainModel(): AddCustomTokenForm.Raw? {
|
||||
return if (values.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
AddCustomTokenForm.Raw(
|
||||
contractAddress = values.getOrElse(index = 0) { "" },
|
||||
name = values.getOrElse(index = 1) { "" },
|
||||
symbol = values.getOrElse(index = 2) { "" },
|
||||
decimals = values.getOrElse(index = 3) { "" },
|
||||
fields = to.fields.mapValues { (key, field) ->
|
||||
field.copy(
|
||||
value = when (key) {
|
||||
Field.CONTRACT_ADDRESS -> contractAddress
|
||||
Field.NAME -> name
|
||||
Field.SYMBOL -> symbol
|
||||
Field.DECIMALS -> decimals
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}.toPersistentMap(),
|
||||
)
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.managetokens.model
|
|||
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -15,22 +16,27 @@ import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationE
|
|||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
|
||||
import com.tangem.features.managetokens.component.CustomTokenFormComponent
|
||||
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM.Field
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues
|
||||
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.utils.CustomCurrencyFormBuilder
|
||||
import com.tangem.features.managetokens.utils.CustomCurrencyValidator
|
||||
import com.tangem.features.managetokens.utils.mapper.mapToDomainModel
|
||||
import com.tangem.features.managetokens.utils.ui.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ComponentScoped
|
||||
internal class CustomTokenFormModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -38,6 +44,8 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val customTokenFormManager: CustomCurrencyFormBuilder,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -68,20 +76,23 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
return CustomTokenFormUM(
|
||||
networkName = ClickableFieldUM(
|
||||
label = resourceReference(R.string.custom_token_network_input_title),
|
||||
value = params.network.name,
|
||||
value = stringReference(params.network.name),
|
||||
onClick = ::selectNetwork,
|
||||
),
|
||||
tokenForm = if (params.network.canHandleTokens) {
|
||||
getInitialTokenForm()
|
||||
customTokenFormManager.buildForm(
|
||||
updateFormFieldValue = ::updateFormFieldValue,
|
||||
updateFormFieldFocus = ::updateFormFieldFocus,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
derivationPath = ClickableFieldUM(
|
||||
label = resourceReference(R.string.custom_token_derivation_path),
|
||||
value = if (params.derivationPath == null || params.derivationPath.id == params.network.id) {
|
||||
value = if (params.derivationPath == null || params.derivationPath.isDefault) {
|
||||
resourceReference(R.string.custom_token_derivation_path_default)
|
||||
} else {
|
||||
params.derivationPath.networkName
|
||||
stringReference(params.derivationPath.name)
|
||||
},
|
||||
onClick = ::selectDerivationPath,
|
||||
),
|
||||
|
|
@ -241,79 +252,52 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getInitialTokenForm(): CustomTokenFormUM.TokenFormUM {
|
||||
val formValues = params.formValues
|
||||
|
||||
val form = CustomTokenFormUM.TokenFormUM(
|
||||
contractAddress = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_contract_address_input_title),
|
||||
placeholder = stringReference(CONTRACT_ADDRESS_PLACEHOLDER),
|
||||
onValueChange = ::updateContractAddress,
|
||||
),
|
||||
name = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_name_input_title),
|
||||
placeholder = resourceReference(R.string.custom_token_name_input_placeholder),
|
||||
onValueChange = ::updateTokenName,
|
||||
),
|
||||
symbol = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_token_symbol_input_title),
|
||||
placeholder = resourceReference(R.string.custom_token_token_symbol_input_placeholder),
|
||||
onValueChange = ::updateTokenSymbol,
|
||||
),
|
||||
decimals = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_decimals_input_title),
|
||||
placeholder = stringReference(DECIMALS_PLACEHOLDER),
|
||||
onValueChange = ::updateDecimals,
|
||||
),
|
||||
)
|
||||
|
||||
return formValues.fillValues(form)
|
||||
}
|
||||
|
||||
private fun getDerivationPath(): Network.DerivationPath {
|
||||
return params.derivationPath?.value ?: params.network.derivationPath
|
||||
}
|
||||
|
||||
private fun updateContractAddress(value: String) {
|
||||
private fun updateFormFieldValue(field: Field, value: String) {
|
||||
state.update { state ->
|
||||
state.updateTokenForm {
|
||||
val fieldValue = fields.getValue(field)
|
||||
|
||||
if (!fieldValue.isEnabled) return@updateTokenForm this
|
||||
|
||||
val updatedFieldValue = fieldValue.copy(
|
||||
value = value,
|
||||
)
|
||||
val updatedFields = fields.mutate {
|
||||
it[field] = updatedFieldValue
|
||||
}
|
||||
|
||||
copy(
|
||||
contractAddress = contractAddress.updateValue(value),
|
||||
fields = updatedFields,
|
||||
wasFilled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTokenName(value: String) {
|
||||
private fun updateFormFieldFocus(field: Field, isFocused: Boolean) {
|
||||
state.update { state ->
|
||||
state.updateTokenForm {
|
||||
copy(
|
||||
name = name.updateValue(value),
|
||||
wasFilled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val fieldValue = fields.getValue(field)
|
||||
|
||||
private fun updateTokenSymbol(value: String) {
|
||||
state.update { state ->
|
||||
state.updateTokenForm {
|
||||
copy(
|
||||
symbol = symbol.updateValue(value),
|
||||
wasFilled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!fieldValue.isEnabled) return@updateTokenForm this
|
||||
|
||||
private fun updateDecimals(value: String) {
|
||||
state.update { state ->
|
||||
state.updateTokenForm {
|
||||
copy(
|
||||
decimals = decimals.updateValue(value),
|
||||
wasFilled = false,
|
||||
val updatedFieldValue = fieldValue.copy(
|
||||
isFocused = isFocused,
|
||||
)
|
||||
val updatedFields = fields.mutate {
|
||||
it[field] = updatedFieldValue
|
||||
}
|
||||
|
||||
// Checking if a field is out of focus
|
||||
if (fieldValue.isFocused && !isFocused && fieldValue.value.isNotEmpty()) {
|
||||
sendFieldAnalyticsEvent(field, fieldValue)
|
||||
}
|
||||
|
||||
copy(fields = updatedFields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -337,6 +321,13 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
return@resource
|
||||
}
|
||||
|
||||
val event = CustomTokenAnalyticsEvent.CustomTokenWasAdded(
|
||||
currencySymbol = currency.symbol,
|
||||
derivationPath = currency.network.derivationPath.value.orEmpty(),
|
||||
source = params.source,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
|
||||
derivePublicKeysUseCase(params.userWalletId, listOf(currency)).getOrElse {
|
||||
Timber.e(it, "Failed to derive public keys")
|
||||
showErrorDialog()
|
||||
|
|
@ -360,8 +351,17 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
params.onSelectDerivationPathClick(CustomTokenFormValues(state.value.tokenForm))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CONTRACT_ADDRESS_PLACEHOLDER = "0x000000000000000000000000000..."
|
||||
const val DECIMALS_PLACEHOLDER = "0"
|
||||
private fun sendFieldAnalyticsEvent(field: Field, fieldValue: TextInputFieldUM) {
|
||||
val event = when (field) {
|
||||
Field.CONTRACT_ADDRESS -> CustomTokenAnalyticsEvent.Address(
|
||||
isValid = fieldValue.error == null,
|
||||
source = params.source,
|
||||
)
|
||||
Field.NAME -> CustomTokenAnalyticsEvent.Name(params.source)
|
||||
Field.SYMBOL -> CustomTokenAnalyticsEvent.Symbol(params.source)
|
||||
Field.DECIMALS -> CustomTokenAnalyticsEvent.Decimals(params.source)
|
||||
}
|
||||
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.managetokens.GetSupportedNetworksUseCase
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
|
|
@ -90,7 +89,7 @@ internal class CustomTokenSelectorModel @Inject constructor(
|
|||
onSelectedStateChange = {
|
||||
val model = SelectedNetwork(
|
||||
id = network.id,
|
||||
name = stringReference(network.name),
|
||||
name = network.name,
|
||||
derivationPath = network.derivationPath,
|
||||
canHandleTokens = network.canHandleTokens,
|
||||
)
|
||||
|
|
@ -109,8 +108,9 @@ internal class CustomTokenSelectorModel @Inject constructor(
|
|||
onSelectedStateChange = {
|
||||
val model = SelectedDerivationPath(
|
||||
id = network.id,
|
||||
networkName = resourceReference(R.string.custom_token_derivation_path_default),
|
||||
name = network.name,
|
||||
value = network.derivationPath,
|
||||
isDefault = true,
|
||||
)
|
||||
|
||||
selector.onDerivationPathSelected(model)
|
||||
|
|
@ -133,8 +133,9 @@ internal class CustomTokenSelectorModel @Inject constructor(
|
|||
onSelectedStateChange = {
|
||||
val model = SelectedDerivationPath(
|
||||
id = network.id,
|
||||
networkName = stringReference(network.name),
|
||||
name = network.name,
|
||||
value = network.derivationPath,
|
||||
isDefault = false,
|
||||
)
|
||||
|
||||
selector.onDerivationPathSelected(model)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.features.managetokens.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -16,6 +18,8 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.managetokens.SaveManagedTokensUseCase
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
|
||||
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig
|
||||
|
|
@ -35,6 +39,7 @@ import kotlinx.coroutines.launch
|
|||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ComponentScoped
|
||||
internal class ManageTokensModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -42,6 +47,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
private val manageTokensListManager: ManageTokensListManager,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -68,7 +74,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
observeSearchQueryChanges()
|
||||
|
||||
modelScope.launch {
|
||||
manageTokensListManager.launchPagination(params.userWalletId)
|
||||
manageTokensListManager.launchPagination(params)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,6 +85,8 @@ internal class ManageTokensModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getInitialState(userWalletId: UserWalletId?): ManageTokensUM {
|
||||
analyticsEventHandler.send(ManageTokensAnalyticEvent.ScreenOpened(params.source))
|
||||
|
||||
return if (userWalletId == null) {
|
||||
createReadContentModel()
|
||||
} else {
|
||||
|
|
@ -140,8 +148,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
state
|
||||
.distinctUntilChanged { old, new ->
|
||||
// It's also used to skip search activation to avoid searching an empty query
|
||||
old.search.query == new.search.query &&
|
||||
(old.search.isActive == new.search.isActive || new.search.isActive)
|
||||
old.search.query == new.search.query && new.search.isActive
|
||||
}
|
||||
.transform { state ->
|
||||
val query = state.search.query
|
||||
|
|
@ -161,11 +168,19 @@ internal class ManageTokensModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun updateItems(items: ImmutableList<CurrencyItemUM>) {
|
||||
state.update { state ->
|
||||
val updatedState = state.updateAndGet { state ->
|
||||
state.copySealed(
|
||||
items = items,
|
||||
)
|
||||
}
|
||||
|
||||
if (updatedState.items.isEmpty() && updatedState.search.isActive) {
|
||||
val event = ManageTokensAnalyticEvent.TokensIsNotFound(
|
||||
query = updatedState.search.query,
|
||||
source = params.source,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePaginationStatus(status: PaginationStatus<*>) {
|
||||
|
|
@ -234,7 +249,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun consumeScrollToTopEvent() {
|
||||
this.state.update { state ->
|
||||
state.update { state ->
|
||||
state.copySealed(
|
||||
scrollToTop = consumedEvent(),
|
||||
)
|
||||
|
|
@ -264,24 +279,33 @@ internal class ManageTokensModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun navigateToAddCustomToken() {
|
||||
analyticsEventHandler.send(CustomTokenAnalyticsEvent.ButtonCustomToken(params.source))
|
||||
|
||||
params.userWalletId?.let {
|
||||
bottomSheetNavigation.activate(ManageTokensBottomSheetConfig.AddCustomToken(it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveChanges() {
|
||||
modelScope.launch {
|
||||
state.update { state -> state.copySealed(isSavingInProgress = true) }
|
||||
saveManagedTokensUseCase.invoke(
|
||||
userWalletId = requireNotNull(params.userWalletId),
|
||||
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
|
||||
currenciesToRemove = manageTokensListManager.currenciesToRemove.value,
|
||||
).fold(
|
||||
ifLeft = { Timber.e(it, "Failed to save changes") },
|
||||
ifRight = { router.pop() },
|
||||
)
|
||||
state.update { state -> state.copySealed(isSavingInProgress = false) }
|
||||
private fun saveChanges() = resource(
|
||||
acquire = { state.update { state -> state.copySealed(isSavingInProgress = true) } },
|
||||
release = { state.update { state -> state.copySealed(isSavingInProgress = false) } },
|
||||
) {
|
||||
val event = ManageTokensAnalyticEvent.TokenAdded(
|
||||
tokensCount = manageTokensListManager.currenciesToAdd.value.values.sumOf { it.size },
|
||||
source = params.source,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
|
||||
saveManagedTokensUseCase(
|
||||
userWalletId = requireNotNull(params.userWalletId),
|
||||
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
|
||||
currenciesToRemove = manageTokensListManager.currenciesToRemove.value,
|
||||
).getOrElse {
|
||||
Timber.e(it, "Failed to save changes")
|
||||
return@resource
|
||||
}
|
||||
|
||||
router.pop()
|
||||
}
|
||||
|
||||
private fun searchCurrencies(query: String) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ 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.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.domain.tokens.model.Network
|
||||
|
|
@ -102,7 +101,7 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<
|
|||
popBack = {},
|
||||
selectedNetwork = SelectedNetwork(
|
||||
id = Network.ID(value = "1"),
|
||||
name = stringReference("Ethereum"),
|
||||
name = "Ethereum",
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
canHandleTokens = false,
|
||||
),
|
||||
|
|
@ -115,7 +114,7 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<
|
|||
popBack = {},
|
||||
selectedNetwork = SelectedNetwork(
|
||||
id = Network.ID(value = "0"),
|
||||
name = stringReference("Ethereum"),
|
||||
name = "Ethereum",
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
canHandleTokens = false,
|
||||
),
|
||||
|
|
@ -129,7 +128,8 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<
|
|||
selectedDerivationPath = SelectedDerivationPath(
|
||||
id = Network.ID(value = "0"),
|
||||
value = Network.DerivationPath.None,
|
||||
networkName = stringReference("Ethereum"),
|
||||
name = "Ethereum",
|
||||
isDefault = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,19 +7,16 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
|
@ -37,9 +34,11 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenFormComponent
|
||||
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM.Field
|
||||
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.ui.component.AddCustomTokenDescription
|
||||
import kotlinx.collections.immutable.mutate
|
||||
|
||||
@Composable
|
||||
internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier = Modifier) {
|
||||
|
|
@ -125,43 +124,14 @@ private fun TokenForm(tokenForm: CustomTokenFormUM.TokenFormUM, modifier: Modifi
|
|||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
) {
|
||||
TextField(
|
||||
model = tokenForm.contractAddress,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
imeAction = ImeAction.Next,
|
||||
),
|
||||
)
|
||||
TextField(
|
||||
model = tokenForm.name,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
imeAction = ImeAction.Next,
|
||||
),
|
||||
)
|
||||
TextField(
|
||||
model = tokenForm.symbol,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
imeAction = ImeAction.Next,
|
||||
),
|
||||
)
|
||||
TextField(
|
||||
model = tokenForm.decimals,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Decimal,
|
||||
imeAction = ImeAction.Next,
|
||||
),
|
||||
)
|
||||
tokenForm.fields.values.forEach { field ->
|
||||
TextField(model = field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextField(
|
||||
model: TextInputFieldUM,
|
||||
modifier: Modifier = Modifier,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
var isFocused by remember { mutableStateOf(value = false) }
|
||||
|
||||
private fun TextField(model: TextInputFieldUM, modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
|
|
@ -173,7 +143,7 @@ private fun TextField(
|
|||
model.error != null -> {
|
||||
TangemTheme.colors.text.warning
|
||||
}
|
||||
model.value.isNotBlank() || isFocused -> {
|
||||
model.value.isNotBlank() || model.isFocused -> {
|
||||
TangemTheme.colors.text.tertiary
|
||||
}
|
||||
else -> {
|
||||
|
|
@ -204,16 +174,15 @@ private fun TextField(
|
|||
.padding(bottom = TangemTheme.dimens.spacing12)
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged {
|
||||
isFocused = it.isFocused
|
||||
model.onFocusChange(it.isFocused)
|
||||
},
|
||||
value = model.value,
|
||||
color = color,
|
||||
onValueChange = model.onValueChange,
|
||||
placeholder = model.placeholder,
|
||||
readOnly = !model.isEnabled && !isFocused,
|
||||
readOnly = !model.isEnabled && !model.isFocused,
|
||||
singleLine = true,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
keyboardOptions = model.keyboardOptions,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -262,25 +231,31 @@ private class PreviewCustomTokenFormComponentProvider :
|
|||
override val values: Sequence<PreviewCustomTokenFormComponent>
|
||||
get() = sequenceOf(
|
||||
PreviewCustomTokenFormComponent(
|
||||
tokenForm = PreviewCustomTokenFormComponent.tokenForm.copy(
|
||||
contractAddress = TextInputFieldUM(
|
||||
label = stringReference("Contract address"),
|
||||
value = "0x1234567890",
|
||||
placeholder = stringReference("0x1234567890"),
|
||||
onValueChange = {},
|
||||
),
|
||||
),
|
||||
tokenForm = PreviewCustomTokenFormComponent.tokenForm.let { form ->
|
||||
form.copy(
|
||||
fields = form.fields.mutate {
|
||||
it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy(
|
||||
label = stringReference("Contract address"),
|
||||
value = "0x1234567890",
|
||||
placeholder = stringReference("0x1234567890"),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
PreviewCustomTokenFormComponent(
|
||||
tokenForm = PreviewCustomTokenFormComponent.tokenForm.copy(
|
||||
contractAddress = TextInputFieldUM(
|
||||
label = stringReference("Contract address"),
|
||||
value = "0x1234567890",
|
||||
error = stringReference("Contract address is invalid"),
|
||||
placeholder = stringReference("0x1234567890"),
|
||||
onValueChange = {},
|
||||
),
|
||||
),
|
||||
tokenForm = PreviewCustomTokenFormComponent.tokenForm.let { form ->
|
||||
form.copy(
|
||||
fields = form.fields.mutate {
|
||||
it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy(
|
||||
label = stringReference("Contract address"),
|
||||
value = "0x1234567890",
|
||||
error = stringReference("Contract address is invalid"),
|
||||
placeholder = stringReference("0x1234567890"),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
PreviewCustomTokenFormComponent(
|
||||
tokenForm = null,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
|||
import com.tangem.core.ui.components.rows.ChainRow
|
||||
import com.tangem.core.ui.components.rows.model.ChainRowUM
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
|
|
@ -271,14 +270,15 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider :
|
|||
userWalletId = UserWalletId(stringValue = "321"),
|
||||
selectedNetwork = SelectedNetwork(
|
||||
id = Network.ID(value = "0"),
|
||||
name = stringReference("Ethereum"),
|
||||
name = "Ethereum",
|
||||
derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"),
|
||||
canHandleTokens = true,
|
||||
),
|
||||
selectedDerivationPath = SelectedDerivationPath(
|
||||
id = Network.ID(value = "0"),
|
||||
value = Network.DerivationPath.Card("m/44'/0'/0'/0/0"),
|
||||
networkName = stringReference(""),
|
||||
name = "",
|
||||
isDefault = false,
|
||||
),
|
||||
onDerivationPathSelected = {},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
package com.tangem.features.managetokens.utils
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.managetokens.component.CustomTokenFormComponent
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM.Field
|
||||
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class CustomCurrencyFormBuilder @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
) {
|
||||
|
||||
private val params: CustomTokenFormComponent.Params = paramsContainer.require()
|
||||
|
||||
fun buildForm(
|
||||
updateFormFieldValue: (Field, String) -> Unit,
|
||||
updateFormFieldFocus: (Field, Boolean) -> Unit,
|
||||
): CustomTokenFormUM.TokenFormUM {
|
||||
val formValues = params.formValues
|
||||
val fields = persistentMapOf(
|
||||
Field.CONTRACT_ADDRESS to TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_contract_address_input_title),
|
||||
placeholder = stringReference(CONTRACT_ADDRESS_PLACEHOLDER),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
onValueChange = { value ->
|
||||
updateFormFieldValue(Field.CONTRACT_ADDRESS, value)
|
||||
},
|
||||
onFocusChange = { isFocused ->
|
||||
updateFormFieldFocus(Field.CONTRACT_ADDRESS, isFocused)
|
||||
},
|
||||
),
|
||||
Field.NAME to TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_name_input_title),
|
||||
placeholder = resourceReference(R.string.custom_token_name_input_placeholder),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Words,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
onValueChange = { value ->
|
||||
updateFormFieldValue(Field.NAME, value)
|
||||
},
|
||||
onFocusChange = { isFocused ->
|
||||
updateFormFieldFocus(Field.NAME, isFocused)
|
||||
},
|
||||
),
|
||||
Field.SYMBOL to TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_token_symbol_input_title),
|
||||
placeholder = resourceReference(R.string.custom_token_token_symbol_input_placeholder),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Characters,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
onValueChange = { value ->
|
||||
updateFormFieldValue(Field.SYMBOL, value)
|
||||
},
|
||||
onFocusChange = { isFocused ->
|
||||
updateFormFieldFocus(Field.SYMBOL, isFocused)
|
||||
},
|
||||
),
|
||||
Field.DECIMALS to TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_decimals_input_title),
|
||||
placeholder = stringReference(DECIMALS_PLACEHOLDER),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
onValueChange = { value ->
|
||||
updateFormFieldValue(Field.DECIMALS, value)
|
||||
},
|
||||
onFocusChange = { isFocused ->
|
||||
updateFormFieldFocus(Field.DECIMALS, isFocused)
|
||||
},
|
||||
),
|
||||
)
|
||||
val form = CustomTokenFormUM.TokenFormUM(fields)
|
||||
|
||||
return formValues.fillValues(form)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CONTRACT_ADDRESS_PLACEHOLDER = "0x000000000000000000000000000..."
|
||||
const val DECIMALS_PLACEHOLDER = "0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.managetokens.utils.list
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
|
|
@ -8,12 +9,15 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
|
||||
import com.tangem.domain.managetokens.CheckHasLinkedTokensUseCase
|
||||
import com.tangem.domain.managetokens.GetManagedTokensUseCase
|
||||
import com.tangem.domain.managetokens.RemoveCustomManagedCryptoCurrencyUseCase
|
||||
import com.tangem.domain.managetokens.CheckHasLinkedTokensUseCase
|
||||
import com.tangem.domain.managetokens.model.*
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.pagination.BatchAction
|
||||
|
|
@ -42,10 +46,12 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
clipboardManager: ClipboardManager,
|
||||
) : ManageTokensUiActions {
|
||||
|
||||
private lateinit var scope: CoroutineScope
|
||||
private lateinit var source: ManageTokensSource
|
||||
|
||||
private val jobHolder = JobHolder()
|
||||
private val actionsFlow: MutableSharedFlow<ManageTokensBatchAction> = MutableSharedFlow(
|
||||
|
|
@ -74,8 +80,9 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
.distinctUntilChanged()
|
||||
val uiItems: Flow<ImmutableList<CurrencyItemUM>> = uiManager.items
|
||||
|
||||
suspend fun launchPagination(userWalletId: UserWalletId?) = coroutineScope {
|
||||
suspend fun launchPagination(params: ManageTokensComponent.Params) = coroutineScope {
|
||||
scope = this
|
||||
source = params.source
|
||||
|
||||
val batchFlow = getManagedTokensUseCase(
|
||||
context = ManageTokensListBatchingContext(
|
||||
|
|
@ -85,13 +92,13 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
)
|
||||
|
||||
batchFlow.state
|
||||
.onEach { state -> updateState(state, userWalletId) }
|
||||
.onEach { state -> updateState(state, params.userWalletId) }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(scope = this)
|
||||
.saveIn(jobHolder)
|
||||
|
||||
// Initial load
|
||||
reload(userWalletId)
|
||||
reload(params.userWalletId)
|
||||
}
|
||||
|
||||
suspend fun reload(userWalletId: UserWalletId?) {
|
||||
|
|
@ -159,12 +166,16 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
changedCurrenciesManager.addCurrency(currency, network)
|
||||
|
||||
sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = true)
|
||||
|
||||
sendSelectCurrencyAnalyticsEvent(currency, isSelected = true)
|
||||
}
|
||||
|
||||
override fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) {
|
||||
changedCurrenciesManager.removeCurrency(currency, network)
|
||||
|
||||
sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false)
|
||||
|
||||
sendSelectCurrencyAnalyticsEvent(currency, isSelected = false)
|
||||
}
|
||||
|
||||
override fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) {
|
||||
|
|
@ -180,6 +191,15 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
network: Network,
|
||||
): Boolean = !changedCurrenciesManager.containsCurrency(currency, network)
|
||||
|
||||
private fun sendSelectCurrencyAnalyticsEvent(currency: ManagedCryptoCurrency.Token, isSelected: Boolean) {
|
||||
val event = ManageTokensAnalyticEvent.TokenSwitcherChanged(
|
||||
tokenSymbol = currency.symbol,
|
||||
isSelected = isSelected,
|
||||
source = source,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
|
||||
private fun sendSelectCurrencyAction(
|
||||
batchKey: Int,
|
||||
currencyId: ManagedCryptoCurrency.ID,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
|
|||
|
||||
internal fun CustomTokenFormUM.TokenFormUM.mapToDomainModel(): AddCustomTokenForm.Raw {
|
||||
return AddCustomTokenForm.Raw(
|
||||
contractAddress = contractAddress.value,
|
||||
symbol = symbol.value,
|
||||
name = name.value,
|
||||
decimals = decimals.value,
|
||||
contractAddress = fields.getValue(CustomTokenFormUM.TokenFormUM.Field.CONTRACT_ADDRESS).value,
|
||||
symbol = fields.getValue(CustomTokenFormUM.TokenFormUM.Field.SYMBOL).value,
|
||||
name = fields.getValue(CustomTokenFormUM.TokenFormUM.Field.NAME).value,
|
||||
decimals = fields.getValue(CustomTokenFormUM.TokenFormUM.Field.DECIMALS).value,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
package com.tangem.features.managetokens.utils.ui
|
||||
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
|
||||
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM.Field
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
|
||||
internal fun CustomTokenFormUM.updateTokenForm(
|
||||
block: CustomTokenFormUM.TokenFormUM.() -> CustomTokenFormUM.TokenFormUM,
|
||||
|
|
@ -23,19 +23,6 @@ internal fun CustomTokenFormUM.updateTokenForm(
|
|||
return copy(tokenForm = updatedForm)
|
||||
}
|
||||
|
||||
internal fun TextInputFieldUM.updateValue(
|
||||
value: String = this.value,
|
||||
error: TextReference? = this.error,
|
||||
isEnabled: Boolean = this.isEnabled,
|
||||
clearError: Boolean = false,
|
||||
): TextInputFieldUM {
|
||||
return copy(
|
||||
value = value,
|
||||
error = if (clearError) null else error,
|
||||
isEnabled = isEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithProgress(
|
||||
showProgress: Boolean,
|
||||
isWasFilled: Boolean = this.tokenForm?.wasFilled ?: false,
|
||||
|
|
@ -49,22 +36,21 @@ internal fun CustomTokenFormUM.updateWithProgress(
|
|||
canAddToken = canAddToken,
|
||||
notifications = if (clearNotifications) persistentListOf() else notifications,
|
||||
).updateTokenForm {
|
||||
val updatedFields = fields.mapValues { (key, field) ->
|
||||
field.copy(
|
||||
isEnabled = when (key) {
|
||||
Field.CONTRACT_ADDRESS -> field.isEnabled
|
||||
Field.NAME,
|
||||
Field.SYMBOL,
|
||||
Field.DECIMALS,
|
||||
-> !(showProgress || disableSecondaryFields)
|
||||
},
|
||||
error = if (clearFieldErrors) null else field.error,
|
||||
)
|
||||
}
|
||||
|
||||
copy(
|
||||
contractAddress = contractAddress.updateValue(
|
||||
clearError = clearFieldErrors,
|
||||
),
|
||||
name = name.updateValue(
|
||||
isEnabled = !showProgress && !disableSecondaryFields,
|
||||
clearError = clearFieldErrors,
|
||||
),
|
||||
symbol = symbol.updateValue(
|
||||
isEnabled = !showProgress && !disableSecondaryFields,
|
||||
clearError = clearFieldErrors,
|
||||
),
|
||||
decimals = decimals.updateValue(
|
||||
isEnabled = !showProgress && !disableSecondaryFields,
|
||||
clearError = clearFieldErrors,
|
||||
),
|
||||
fields = updatedFields.toPersistentMap(),
|
||||
wasFilled = isWasFilled,
|
||||
)
|
||||
}
|
||||
|
|
@ -72,11 +58,31 @@ internal fun CustomTokenFormUM.updateWithProgress(
|
|||
|
||||
internal fun CustomTokenFormUM.updateWithCurrency(currency: CryptoCurrency): CustomTokenFormUM {
|
||||
return updateTokenForm {
|
||||
val updatedFields = fields.mapValues { (key, field) ->
|
||||
when (key) {
|
||||
Field.CONTRACT_ADDRESS -> field.copy(
|
||||
error = null,
|
||||
)
|
||||
Field.NAME -> field.copy(
|
||||
value = currency.name,
|
||||
error = null,
|
||||
isEnabled = false,
|
||||
)
|
||||
Field.SYMBOL -> field.copy(
|
||||
value = currency.symbol,
|
||||
error = null,
|
||||
isEnabled = false,
|
||||
)
|
||||
Field.DECIMALS -> field.copy(
|
||||
value = currency.decimals.toString(),
|
||||
error = null,
|
||||
isEnabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
copy(
|
||||
contractAddress = contractAddress.updateValue(error = null),
|
||||
name = name.updateValue(currency.name),
|
||||
symbol = symbol.updateValue(currency.symbol),
|
||||
decimals = decimals.updateValue(currency.decimals.toString()),
|
||||
fields = updatedFields.toPersistentMap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -85,18 +91,20 @@ internal fun CustomTokenFormUM.updateWithContractAddressException(
|
|||
exception: CustomTokenFormValidationException.ContractAddress,
|
||||
): CustomTokenFormUM {
|
||||
return updateTokenForm {
|
||||
copy(
|
||||
contractAddress = contractAddress.updateValue(
|
||||
val updatedFields = fields.mutate {
|
||||
it[Field.CONTRACT_ADDRESS] = it.getValue(Field.CONTRACT_ADDRESS).copy(
|
||||
error = when (exception) {
|
||||
CustomTokenFormValidationException.ContractAddress.Empty -> {
|
||||
null
|
||||
null // Should not display this error
|
||||
}
|
||||
CustomTokenFormValidationException.ContractAddress.Invalid -> {
|
||||
resourceReference(R.string.custom_token_creation_error_invalid_contract_address)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
copy(fields = updatedFields)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,11 +112,11 @@ internal fun CustomTokenFormUM.updateWithDecimalsException(
|
|||
exception: CustomTokenFormValidationException.Decimals,
|
||||
): CustomTokenFormUM {
|
||||
return updateTokenForm {
|
||||
copy(
|
||||
decimals = decimals.updateValue(
|
||||
val updatedFields = fields.mutate {
|
||||
it[Field.DECIMALS] = it.getValue(Field.DECIMALS).copy(
|
||||
error = when (exception) {
|
||||
is CustomTokenFormValidationException.Decimals.Empty -> {
|
||||
null
|
||||
null // Should not display this error
|
||||
}
|
||||
is CustomTokenFormValidationException.Decimals.Invalid -> {
|
||||
resourceReference(
|
||||
|
|
@ -117,8 +125,10 @@ internal fun CustomTokenFormUM.updateWithDecimalsException(
|
|||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
copy(fields = updatedFields)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,11 +110,8 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
|
|||
backgroundColor = LocalMainBottomSheetColor.current.value,
|
||||
addTopBarStatusBarPadding = false,
|
||||
state = state,
|
||||
onBackClick = {
|
||||
if (bsState == BottomSheetState.EXPANDED) {
|
||||
navigateBack()
|
||||
}
|
||||
},
|
||||
onBackClick = ::navigateBack,
|
||||
backButtonEnabled = bsState == BottomSheetState.EXPANDED,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
portfolioBlock = portfolioComponent?.let { component ->
|
||||
{ blockModifier ->
|
||||
|
|
@ -145,6 +142,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
|
|||
addTopBarStatusBarPadding = true,
|
||||
state = state,
|
||||
onBackClick = ::navigateBack,
|
||||
backButtonEnabled = true,
|
||||
onHeaderSizeChange = {},
|
||||
portfolioBlock = portfolioComponent?.let { component ->
|
||||
{ blockModifier ->
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import java.math.BigDecimal
|
|||
@Stable
|
||||
internal class MetricsConverter(
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val tokenSymbol: String,
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
) : Converter<TokenMarketInfo.Metrics, MetricsUM> {
|
||||
|
||||
|
|
@ -99,12 +100,12 @@ internal class MetricsConverter(
|
|||
},
|
||||
),
|
||||
InfoPointUM(
|
||||
title = resourceReference(R.string.markets_token_details_total_supply),
|
||||
value = totalSupply.formatAmount(crypto = true),
|
||||
title = resourceReference(R.string.markets_token_details_max_supply),
|
||||
value = maxSupply.formatMaxSupply(),
|
||||
onInfoClick = {
|
||||
onInfoClick(
|
||||
InfoBottomSheetContent(
|
||||
title = resourceReference(R.string.markets_token_details_total_supply_full),
|
||||
title = resourceReference(R.string.markets_token_details_max_supply_full),
|
||||
body = resourceReference(R.string.markets_token_details_total_supply_description),
|
||||
),
|
||||
)
|
||||
|
|
@ -115,13 +116,26 @@ internal class MetricsConverter(
|
|||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal?.formatMaxSupply(): String {
|
||||
when (this) {
|
||||
null -> return StringsSigns.DASH_SIGN
|
||||
BigDecimal.ZERO -> return StringsSigns.INFINITY_SIGN
|
||||
}
|
||||
|
||||
return this.formatAmount(crypto = true)
|
||||
}
|
||||
|
||||
private fun BigDecimal?.formatAmount(crypto: Boolean = false): String {
|
||||
if (this == null) return StringsSigns.DASH_SIGN
|
||||
|
||||
return if (crypto) {
|
||||
BigDecimalFormatter.formatCompactAmount(amount = this)
|
||||
BigDecimalFormatter.formatCompactCryptoAmount(
|
||||
amount = this,
|
||||
cryptoCurrencySymbol = tokenSymbol,
|
||||
)
|
||||
} else {
|
||||
val currency = appCurrency()
|
||||
|
||||
BigDecimalFormatter.formatCompactFiatAmount(
|
||||
amount = this,
|
||||
fiatCurrencyCode = currency.code,
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import com.tangem.utils.converter.Converter
|
|||
|
||||
@Stable
|
||||
internal class TokenMarketInfoConverter(
|
||||
appCurrency: Provider<AppCurrency>,
|
||||
onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
private val appCurrency: Provider<AppCurrency>,
|
||||
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
|
||||
onLinkClick: (LinksUM.Link) -> Unit,
|
||||
onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
onInsightsIntervalChanged: (PriceChangeInterval) -> Unit,
|
||||
|
|
@ -28,7 +28,6 @@ internal class TokenMarketInfoConverter(
|
|||
@Suppress("UnusedPrivateMember")
|
||||
// TODO second markets iteration
|
||||
private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick)
|
||||
private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
|
||||
private val pricePerformanceConverter = PricePerformanceConverter(
|
||||
appCurrency = appCurrency,
|
||||
onIntervalChanged = onPricePerformanceIntervalChanged,
|
||||
|
|
@ -36,6 +35,12 @@ internal class TokenMarketInfoConverter(
|
|||
private val linksConverter = LinksConverter(onLinkClick = onLinkClick)
|
||||
|
||||
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks {
|
||||
val metricsConverter = MetricsConverter(
|
||||
tokenSymbol = value.symbol,
|
||||
appCurrency = appCurrency,
|
||||
onInfoClick = onInfoClick,
|
||||
)
|
||||
|
||||
return MarketsTokenDetailsUM.InformationBlocks(
|
||||
insights = value.insights?.let { insightsConverter.convert(it) },
|
||||
securityScore = null,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ internal fun MarketsTokenDetailsContent(
|
|||
addTopBarStatusBarPadding: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
backButtonEnabled: Boolean,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -61,6 +62,7 @@ internal fun MarketsTokenDetailsContent(
|
|||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
backButtonEnabled = backButtonEnabled,
|
||||
portfolioBlock = portfolioBlock,
|
||||
addTopBarStatusBarInsets = addTopBarStatusBarPadding,
|
||||
)
|
||||
|
|
@ -76,6 +78,7 @@ private fun Content(
|
|||
addTopBarStatusBarInsets: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
backButtonEnabled: Boolean,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -98,7 +101,10 @@ private fun Content(
|
|||
}
|
||||
},
|
||||
title = state.tokenName,
|
||||
startButton = TopAppBarButtonUM.Back(onBackClick),
|
||||
startButton = TopAppBarButtonUM.Back(
|
||||
onBackClicked = onBackClick,
|
||||
enabled = backButtonEnabled,
|
||||
),
|
||||
)
|
||||
|
||||
SpacerH4()
|
||||
|
|
@ -305,6 +311,7 @@ private fun Preview() {
|
|||
onBackClick = {},
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
portfolioBlock = {},
|
||||
backButtonEnabled = true,
|
||||
addTopBarStatusBarPadding = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
contentHorizontalPadding = 0.dp,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.markets_token_details_links),
|
||||
|
|
@ -81,7 +82,7 @@ private fun SubBlock(
|
|||
showDivider = !lastBlock,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
|
|
@ -111,6 +112,7 @@ private fun SubBlock(
|
|||
fun LinksBlockPlaceholder(modifier: Modifier = Modifier) {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
contentHorizontalPadding = 0.dp,
|
||||
title = {
|
||||
TextShimmer(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
@ -134,7 +136,7 @@ private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolea
|
|||
showDivider = !lastBlock,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
TextShimmer(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.markets.portfolio.impl.model
|
|||
import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
|
|
@ -45,47 +46,51 @@ internal class AddToPortfolioBSContentUMFactory(
|
|||
fun create(
|
||||
portfolioData: PortfolioData,
|
||||
portfolioUIData: PortfolioUIData,
|
||||
selectedWallet: UserWallet,
|
||||
alreadyAddedNetworks: Set<String>,
|
||||
selectedWallet: UserWallet?,
|
||||
alreadyAddedNetworks: Set<String>?,
|
||||
): TangemBottomSheetConfig {
|
||||
return TangemBottomSheetConfig(
|
||||
isShow = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility,
|
||||
onDismissRequest = { onAddToPortfolioVisibilityChange(false) },
|
||||
content = AddToPortfolioBSContentUM(
|
||||
selectedWallet = selectedWallet.toSelectedUserWalletItemUM(),
|
||||
selectNetworkUM = SelectNetworkUMConverter(
|
||||
networksWithToggle = portfolioUIData.addToPortfolioData.associateWithToggle(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
alreadyAddedNetworkIds = alreadyAddedNetworks,
|
||||
),
|
||||
alreadyAddedNetworks = alreadyAddedNetworks,
|
||||
onNetworkSwitchClick = onNetworkSwitchClick,
|
||||
).convert(value = token),
|
||||
isScanCardNotificationVisible = portfolioUIData.hasMissedDerivations,
|
||||
continueButtonEnabled = portfolioUIData.addToPortfolioData.isUserAddedNetworks(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
),
|
||||
onContinueButtonClick = {
|
||||
val alreadyAddedNetworkIds = portfolioData.walletsWithCurrencies[selectedWallet].orEmpty()
|
||||
.map { it.status.currency.network.backendId }
|
||||
.toSet()
|
||||
|
||||
onContinueClick(
|
||||
selectedWallet.walletId,
|
||||
portfolioUIData.addToPortfolioData.getAddedNetworks(
|
||||
content = if (selectedWallet != null && alreadyAddedNetworks != null) {
|
||||
AddToPortfolioBSContentUM(
|
||||
selectedWallet = selectedWallet.toSelectedUserWalletItemUM(),
|
||||
selectNetworkUM = SelectNetworkUMConverter(
|
||||
networksWithToggle = portfolioUIData.addToPortfolioData.associateWithToggle(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
alreadyAddedNetworkIds = alreadyAddedNetworkIds,
|
||||
alreadyAddedNetworkIds = alreadyAddedNetworks,
|
||||
),
|
||||
)
|
||||
},
|
||||
walletSelectorConfig = crateWalletSelectorBSConfig(
|
||||
isShow = portfolioUIData.portfolioBSVisibilityModel.walletSelectorBSVisibility,
|
||||
portfolioData = portfolioData,
|
||||
selectedWalletId = selectedWallet.walletId,
|
||||
),
|
||||
isWalletBlockVisible = portfolioData.walletsWithCurrencies
|
||||
.filterKeys(UserWallet::isMultiCurrency).size > 1,
|
||||
),
|
||||
alreadyAddedNetworks = alreadyAddedNetworks,
|
||||
onNetworkSwitchClick = onNetworkSwitchClick,
|
||||
).convert(value = token),
|
||||
isScanCardNotificationVisible = portfolioUIData.hasMissedDerivations,
|
||||
continueButtonEnabled = portfolioUIData.addToPortfolioData.isUserAddedNetworks(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
),
|
||||
onContinueButtonClick = {
|
||||
val alreadyAddedNetworkIds = portfolioData.walletsWithCurrencies[selectedWallet].orEmpty()
|
||||
.map { it.status.currency.network.backendId }
|
||||
.toSet()
|
||||
|
||||
onContinueClick(
|
||||
selectedWallet.walletId,
|
||||
portfolioUIData.addToPortfolioData.getAddedNetworks(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
alreadyAddedNetworkIds = alreadyAddedNetworkIds,
|
||||
),
|
||||
)
|
||||
},
|
||||
walletSelectorConfig = crateWalletSelectorBSConfig(
|
||||
isShow = portfolioUIData.portfolioBSVisibilityModel.walletSelectorBSVisibility,
|
||||
portfolioData = portfolioData,
|
||||
selectedWalletId = selectedWallet.walletId,
|
||||
),
|
||||
isWalletBlockVisible = portfolioData.walletsWithCurrencies
|
||||
.filterKeys(UserWallet::isMultiCurrency).size > 1,
|
||||
)
|
||||
} else {
|
||||
TangemBottomSheetConfigContent.Empty
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,9 +30,8 @@ internal class MyPortfolioUMFactory(
|
|||
fun create(portfolioData: PortfolioData, portfolioUIData: PortfolioUIData): MyPortfolioUM {
|
||||
val addToPortfolioData = portfolioUIData.addToPortfolioData
|
||||
|
||||
val hasAvailableNetworks = addToPortfolioData.availableNetworks?.isEmpty() == true
|
||||
val isOnlySingleWalletsAdded = portfolioData.walletsWithCurrencies.keys.all { !it.isMultiCurrency }
|
||||
if (hasAvailableNetworks || isOnlySingleWalletsAdded) return MyPortfolioUM.Unavailable
|
||||
val isOnlyUnavailableNetworks = addToPortfolioData.availableNetworks?.isEmpty() == true
|
||||
if (isOnlyUnavailableNetworks) return MyPortfolioUM.Unavailable
|
||||
|
||||
val walletsWithCurrencies = if (addToPortfolioData.availableNetworks == null) {
|
||||
portfolioData.walletsWithCurrencies
|
||||
|
|
@ -53,7 +52,7 @@ internal class MyPortfolioUMFactory(
|
|||
onAddClick = onAddClick,
|
||||
)
|
||||
} else {
|
||||
MyPortfolioUM.Unavailable
|
||||
MyPortfolioUM.UnavailableForWallet
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,16 +78,13 @@ internal class MyPortfolioUMFactory(
|
|||
val selectedWallet = portfolioData.walletsWithCurrencies.keys
|
||||
.firstOrNull { it.walletId == portfolioUIData.selectedWalletId }
|
||||
?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency }
|
||||
?: error("walletsWithCurrencies don't contain selected wallet or any multi-currency wallet")
|
||||
|
||||
val availableNetworks = portfolioUIData.addToPortfolioData.availableNetworks.orEmpty()
|
||||
|
||||
val alreadyAddedNetworks = requireNotNull(
|
||||
value = portfolioData.walletsWithCurrencies.filterAvailableNetworks(availableNetworks)[selectedWallet],
|
||||
lazyMessage = { "walletsWithCurrencies don't contain ${selectedWallet.walletId}" },
|
||||
)
|
||||
.map { it.status.currency.network.backendId }
|
||||
.toSet()
|
||||
val alreadyAddedNetworks = portfolioData.walletsWithCurrencies
|
||||
.filterAvailableNetworks(availableNetworks)[selectedWallet]
|
||||
?.map { it.status.currency.network.backendId }
|
||||
?.toSet()
|
||||
|
||||
return addToPortfolioBSContentUMFactory.create(
|
||||
portfolioData = portfolioData,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.markets.portfolio.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
|
@ -50,7 +51,8 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) {
|
|||
is MyPortfolioUM.Tokens -> TokenList(state = state)
|
||||
is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state, modifier = contentModifier)
|
||||
MyPortfolioUM.Loading -> LoadingPlaceholder(modifier = contentModifier)
|
||||
MyPortfolioUM.Unavailable -> UnavailableContent(modifier = contentModifier)
|
||||
MyPortfolioUM.Unavailable -> UnavailableAsset(modifier = contentModifier)
|
||||
MyPortfolioUM.UnavailableForWallet -> UnavailableAssetForWallet(modifier = contentModifier)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,10 +113,26 @@ private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun UnavailableContent(modifier: Modifier = Modifier) {
|
||||
fun UnavailableAsset(modifier: Modifier = Modifier) {
|
||||
UnavailableContent(
|
||||
textId = R.string.markets_add_to_my_portfolio_unavailable_description,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UnavailableAssetForWallet(modifier: Modifier = Modifier) {
|
||||
UnavailableContent(
|
||||
textId = R.string.markets_add_to_my_portfolio_unavailable_for_wallet_description,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UnavailableContent(@StringRes textId: Int, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = stringResource(R.string.markets_add_to_my_portfolio_unavailable_description),
|
||||
text = stringResource(textId),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
|
|||
),
|
||||
MyPortfolioUM.Loading,
|
||||
MyPortfolioUM.Unavailable,
|
||||
MyPortfolioUM.UnavailableForWallet,
|
||||
)
|
||||
|
||||
val sampleToken = PortfolioTokenUM(
|
||||
|
|
|
|||
|
|
@ -30,4 +30,6 @@ internal sealed class MyPortfolioUM {
|
|||
data object Loading : MyPortfolioUM()
|
||||
|
||||
data object Unavailable : MyPortfolioUM()
|
||||
|
||||
data object UnavailableForWallet : MyPortfolioUM()
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.analytics
|
|||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionType.Companion.asAnalyticName
|
||||
|
||||
internal sealed class StakingAnalyticsEvents(
|
||||
event: String,
|
||||
|
|
@ -47,18 +48,20 @@ internal sealed class StakingAnalyticsEvents(
|
|||
params = mapOf(
|
||||
AnalyticsParam.TOKEN_PARAM to token,
|
||||
"Validator" to validator,
|
||||
"Action" to action.name,
|
||||
"Action" to action.asAnalyticName,
|
||||
),
|
||||
)
|
||||
|
||||
data class StakeInProgressScreenOpened(
|
||||
val validator: String,
|
||||
val token: String,
|
||||
val action: StakingActionType,
|
||||
) : StakingAnalyticsEvents(
|
||||
event = "Stake In Progress Screen Opened",
|
||||
params = mapOf(
|
||||
AnalyticsParam.TOKEN_PARAM to token,
|
||||
"Validator" to validator,
|
||||
"Action" to action.asAnalyticName,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -136,9 +139,13 @@ internal sealed class StakingAnalyticsEvents(
|
|||
|
||||
data class StakingError(
|
||||
val token: String,
|
||||
val errorType: String,
|
||||
) : StakingAnalyticsEvents(
|
||||
event = "Errors",
|
||||
params = mapOf(AnalyticsParam.TOKEN_PARAM to token),
|
||||
params = mapOf(
|
||||
AnalyticsParam.TOKEN_PARAM to token,
|
||||
AnalyticsParam.ERROR_DESCRIPTION to errorType,
|
||||
),
|
||||
)
|
||||
|
||||
data class TransactionError(
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ internal class StakingAnalyticSender(
|
|||
StakingAnalyticsEvents.StakeInProgressScreenOpened(
|
||||
validator = validatorName,
|
||||
token = value.cryptoCurrencySymbol,
|
||||
action = getStakingActionType(value),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,9 +49,10 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
suspend fun getFee(
|
||||
pendingAction: PendingAction?,
|
||||
pendingActions: ImmutableList<PendingAction>?,
|
||||
onFeeError: (GetFeeError) -> Unit,
|
||||
onStakingFee: (Fee) -> Unit,
|
||||
onStakingFeeError: (StakingError) -> Unit,
|
||||
onApprovalFee: (TransactionFee) -> Unit,
|
||||
onFeeError: (GetFeeError) -> Unit,
|
||||
) {
|
||||
val state = stateController.value
|
||||
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
|
|
@ -85,7 +86,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
pendingActions = pendingActions,
|
||||
amount = amount,
|
||||
validatorAddress = validatorAddress,
|
||||
onFeeError = onFeeError,
|
||||
onStakingFeeError = onStakingFeeError,
|
||||
onStakingFee = onStakingFee,
|
||||
)
|
||||
}
|
||||
|
|
@ -95,7 +96,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
pendingActions = pendingActions,
|
||||
amount = amount,
|
||||
validatorAddress = validatorAddress,
|
||||
onFeeError = onFeeError,
|
||||
onStakingFeeError = onStakingFeeError,
|
||||
onStakingFee = onStakingFee,
|
||||
)
|
||||
}
|
||||
|
|
@ -106,7 +107,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
pendingActions: ImmutableList<PendingAction>?,
|
||||
amount: BigDecimal,
|
||||
validatorAddress: String,
|
||||
onFeeError: (GetFeeError) -> Unit,
|
||||
onStakingFeeError: (StakingError) -> Unit,
|
||||
onStakingFee: (Fee) -> Unit,
|
||||
) {
|
||||
val sourceAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
|
||||
|
|
@ -125,7 +126,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
action = action,
|
||||
)
|
||||
}.getOrElse {
|
||||
onFeeError(GetFeeError.DataError(Throwable(it.toString())))
|
||||
onStakingFeeError(it)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +134,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
}
|
||||
|
||||
if (result.isNullOrEmpty()) {
|
||||
onFeeError(GetFeeError.UnknownError)
|
||||
onStakingFeeError(StakingError.UnknownError)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +152,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
validatorAddress = validatorAddress,
|
||||
action = pendingAction,
|
||||
).getOrElse {
|
||||
onFeeError(GetFeeError.DataError(Throwable(it.toString())))
|
||||
onStakingFeeError(it)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.domain.staking.model.stakekit.Yield
|
|||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
|
|
@ -130,7 +131,10 @@ internal class StakingTransactionSender @AssistedInject constructor(
|
|||
fee: Fee,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
) = coroutineScope {
|
||||
stakingTransactions?.filterNot { it.type == StakingTransactionType.APPROVAL }
|
||||
stakingTransactions
|
||||
?.filterNot {
|
||||
it.type == StakingTransactionType.APPROVAL || it.status == StakingTransactionStatus.SKIPPED
|
||||
}
|
||||
?.map { transaction ->
|
||||
async {
|
||||
getConstructedStakingTransactionUseCase(
|
||||
|
|
@ -150,7 +154,9 @@ internal class StakingTransactionSender @AssistedInject constructor(
|
|||
},
|
||||
)
|
||||
}
|
||||
}?.awaitAll()?.filterNotNull()
|
||||
}
|
||||
?.awaitAll()
|
||||
?.filterNotNull()
|
||||
}
|
||||
|
||||
private suspend fun getStakingTransaction(
|
||||
|
|
@ -185,7 +191,12 @@ internal class StakingTransactionSender @AssistedInject constructor(
|
|||
type = action?.type,
|
||||
),
|
||||
).getOrElse {
|
||||
analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(state.cryptoCurrencyName))
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvents.StakingError(
|
||||
token = state.cryptoCurrencyName,
|
||||
errorType = it.javaClass.simpleName,
|
||||
),
|
||||
)
|
||||
onConstructError(it)
|
||||
return emptyList()
|
||||
}
|
||||
|
|
@ -248,9 +259,12 @@ internal class StakingTransactionSender @AssistedInject constructor(
|
|||
rawCurrencyId = rawCurrencyId,
|
||||
),
|
||||
)
|
||||
.onLeft {
|
||||
.onLeft { error ->
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvents.StakingError(stateController.value.cryptoCurrencyName),
|
||||
StakingAnalyticsEvents.StakingError(
|
||||
token = stateController.value.cryptoCurrencyName,
|
||||
errorType = error.javaClass.simpleName,
|
||||
),
|
||||
)
|
||||
saveUnsubmittedHashUseCase.invoke(
|
||||
transactionId = transactionId,
|
||||
|
|
|
|||
|
|
@ -81,11 +81,11 @@ internal class AddStakingNotificationsTransformer(
|
|||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
).max(minimumRequirement)
|
||||
} else {
|
||||
// No amount is taken from account balance on exit or pending actions
|
||||
BigDecimal.ZERO
|
||||
}.max(minimumRequirement)
|
||||
}
|
||||
|
||||
val notifications = buildList {
|
||||
// errors
|
||||
|
|
|
|||
|
|
@ -32,5 +32,5 @@ internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (t
|
|||
internal fun isSolanaWithdraw(networkId: String, pendingActions: ImmutableList<PendingAction>?): Boolean {
|
||||
val isSolana = isSolana(networkId)
|
||||
val isWithdraw = pendingActions?.all { it.type == StakingActionType.WITHDRAW } == true
|
||||
return isSolana && isWithdraw
|
||||
return isSolana && isWithdraw && !pendingActions.isNullOrEmpty()
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
package com.tangem.features.staking.impl.presentation.viewmodel
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.*
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.routing.AppRoute
|
||||
|
|
@ -256,8 +259,18 @@ internal class StakingViewModel @Inject constructor(
|
|||
)
|
||||
updateNotifications()
|
||||
},
|
||||
onStakingFeeError = { error ->
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvents.StakingError(
|
||||
value.cryptoCurrencyName,
|
||||
error.javaClass.simpleName,
|
||||
),
|
||||
)
|
||||
stateController.update(AddStakingErrorTransformer())
|
||||
updateNotifications(GetFeeError.UnknownError)
|
||||
},
|
||||
onFeeError = { error ->
|
||||
analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName))
|
||||
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName))
|
||||
stateController.update(AddStakingErrorTransformer())
|
||||
updateNotifications(error)
|
||||
},
|
||||
|
|
@ -285,7 +298,12 @@ internal class StakingViewModel @Inject constructor(
|
|||
},
|
||||
onConstructError = { error ->
|
||||
Timber.e(error.toString())
|
||||
analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName))
|
||||
analyticsEventHandler.send(
|
||||
StakingAnalyticsEvents.StakingError(
|
||||
token = value.cryptoCurrencyName,
|
||||
errorType = error.javaClass.simpleName,
|
||||
),
|
||||
)
|
||||
stakingEventFactory.createStakingErrorAlert(error)
|
||||
stateController.update(SetConfirmationStateResetAssentTransformer)
|
||||
},
|
||||
|
|
@ -296,7 +314,7 @@ internal class StakingViewModel @Inject constructor(
|
|||
},
|
||||
onSendError = { error ->
|
||||
Timber.e(error.toString())
|
||||
analyticsEventHandler.send(StakingAnalyticsEvents.StakingError(value.cryptoCurrencyName))
|
||||
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName))
|
||||
stakingEventFactory.createSendTransactionErrorAlert(error)
|
||||
stateController.update(SetConfirmationStateResetAssentTransformer)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -51,10 +51,12 @@ internal class TokenDetailsTxHistoryTransactionStateConverter(
|
|||
is TransactionType.Approve -> R.drawable.ic_doc_24
|
||||
is TransactionType.TronStakingTransactionType.Stake,
|
||||
is TransactionType.TronStakingTransactionType.Vote,
|
||||
-> R.drawable.ic_transaction_history_staking
|
||||
is TransactionType.TronStakingTransactionType.Withdraw,
|
||||
-> R.drawable.ic_transaction_history_staking_24
|
||||
is TransactionType.TronStakingTransactionType.ClaimRewards,
|
||||
-> R.drawable.ic_transaction_history_claim_rewards_24
|
||||
is TransactionType.TronStakingTransactionType.Unstake,
|
||||
-> R.drawable.ic_transaction_history_unstaking
|
||||
is TransactionType.TronStakingTransactionType.Withdraw,
|
||||
-> R.drawable.ic_transaction_history_unstaking_24
|
||||
is TransactionType.Operation,
|
||||
is TransactionType.Swap,
|
||||
is TransactionType.Transfer,
|
||||
|
|
@ -72,6 +74,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter(
|
|||
is TransactionType.TronStakingTransactionType.Stake -> resourceReference(R.string.common_stake)
|
||||
is TransactionType.TronStakingTransactionType.Unstake -> resourceReference(R.string.common_unstake)
|
||||
is TransactionType.TronStakingTransactionType.Vote -> resourceReference(R.string.staking_vote)
|
||||
is TransactionType.TronStakingTransactionType.ClaimRewards -> resourceReference(R.string.common_claim_rewards)
|
||||
is TransactionType.TronStakingTransactionType.Withdraw -> resourceReference(R.string.staking_withdraw)
|
||||
}
|
||||
|
||||
|
|
@ -97,9 +100,13 @@ internal class TokenDetailsTxHistoryTransactionStateConverter(
|
|||
},
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
is InteractionAddressType.Staking -> resourceReference(
|
||||
id = R.string.common_staking,
|
||||
is InteractionAddressType.Validator -> resourceReference(
|
||||
id = R.string.transaction_history_transaction_validator,
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
null -> {
|
||||
TextReference.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractDirection() = if (isOutgoing) Direction.OUTGOING else Direction.INCOMING
|
||||
|
|
@ -111,7 +118,8 @@ internal class TokenDetailsTxHistoryTransactionStateConverter(
|
|||
}
|
||||
|
||||
private fun TxHistoryItem.getAmount(): String {
|
||||
if (type == TransactionType.TronStakingTransactionType.Vote ||
|
||||
if (type is TransactionType.TronStakingTransactionType.Vote ||
|
||||
type == TransactionType.TronStakingTransactionType.ClaimRewards ||
|
||||
type == TransactionType.TronStakingTransactionType.Withdraw
|
||||
) {
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -9,5 +9,7 @@ internal sealed class Settings(
|
|||
error: Throwable? = null,
|
||||
) : AnalyticsEvent(category, event, params, error) {
|
||||
|
||||
class ButtonCreateBackup : Settings(event = "Button - Create Backup")
|
||||
data object ButtonCreateBackup : Settings(event = "Button - Create Backup")
|
||||
|
||||
data object ButtonManageTokens : Settings(event = "Button - Manage Tokens")
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.feature.walletsettings.component.preview
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.analytics.DummyAnalyticsEventHandler
|
||||
import com.tangem.core.decompose.navigation.DummyRouter
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
|
||||
|
|
@ -13,7 +14,10 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
|
|||
|
||||
private val previewState = WalletSettingsUM(
|
||||
popBack = {},
|
||||
items = ItemsBuilder(router = DummyRouter()).buildItems(
|
||||
items = ItemsBuilder(
|
||||
router = DummyRouter(),
|
||||
analyticsEventHandler = DummyAnalyticsEventHandler(),
|
||||
).buildItems(
|
||||
userWalletId = UserWalletId("011"),
|
||||
userWalletName = "My Wallet",
|
||||
isReferralAvailable = true,
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ internal class WalletSettingsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onLinkMoreCardsClick(scanResponse: ScanResponse) {
|
||||
analyticsEventHandler.send(Settings.ButtonCreateBackup())
|
||||
analyticsEventHandler.send(Settings.ButtonCreateBackup)
|
||||
|
||||
analyticsContextProxy.addContext(scanResponse)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package com.tangem.feature.walletsettings.utils
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRoute.ManageTokens.Source
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.components.block.model.BlockUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.walletsettings.analytics.Settings
|
||||
import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM
|
||||
import com.tangem.feature.walletsettings.impl.R
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
|
@ -17,6 +20,7 @@ import javax.inject.Inject
|
|||
@ComponentScoped
|
||||
internal class ItemsBuilder @Inject constructor(
|
||||
private val router: Router,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -62,7 +66,10 @@ internal class ItemsBuilder @Inject constructor(
|
|||
BlockUM(
|
||||
text = resourceReference(R.string.add_tokens_title),
|
||||
iconRes = R.drawable.ic_tether_24,
|
||||
onClick = { router.push(AppRoute.ManageTokens(userWalletId)) },
|
||||
onClick = {
|
||||
analyticsEventHandler.send(Settings.ButtonManageTokens)
|
||||
router.push(AppRoute.ManageTokens(Source.SETTINGS, userWalletId))
|
||||
},
|
||||
).let(::add)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import androidx.navigation.compose.composable
|
|||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRoute.ManageTokens.Source
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
|
|
@ -140,7 +141,7 @@ internal class DefaultWalletRouter(
|
|||
}
|
||||
|
||||
override fun openManageTokensScreen(userWalletId: UserWalletId) {
|
||||
router.push(AppRoute.ManageTokens(userWalletId = userWalletId))
|
||||
router.push(AppRoute.ManageTokens(Source.SETTINGS, userWalletId))
|
||||
}
|
||||
|
||||
override fun openScanFailedDialog(onTryAgain: () -> Unit) {
|
||||
|
|
|
|||
|
|
@ -50,10 +50,12 @@ internal class TxHistoryItemStateConverter(
|
|||
is TransactionType.Approve -> R.drawable.ic_doc_24
|
||||
is TransactionType.TronStakingTransactionType.Stake,
|
||||
is TransactionType.TronStakingTransactionType.Vote,
|
||||
-> R.drawable.ic_transaction_history_staking
|
||||
is TransactionType.TronStakingTransactionType.Withdraw,
|
||||
-> R.drawable.ic_transaction_history_staking_24
|
||||
is TransactionType.TronStakingTransactionType.ClaimRewards,
|
||||
-> R.drawable.ic_transaction_history_claim_rewards_24
|
||||
is TransactionType.TronStakingTransactionType.Unstake,
|
||||
-> R.drawable.ic_transaction_history_unstaking
|
||||
is TransactionType.TronStakingTransactionType.Withdraw,
|
||||
-> R.drawable.ic_transaction_history_unstaking_24
|
||||
is TransactionType.Operation,
|
||||
is TransactionType.Swap,
|
||||
is TransactionType.Transfer,
|
||||
|
|
@ -70,7 +72,8 @@ internal class TxHistoryItemStateConverter(
|
|||
is TransactionType.TronStakingTransactionType.Stake -> resourceReference(R.string.common_stake)
|
||||
is TransactionType.TronStakingTransactionType.Unstake -> resourceReference(R.string.common_unstake)
|
||||
is TransactionType.TronStakingTransactionType.Vote -> resourceReference(R.string.staking_vote)
|
||||
is TransactionType.TronStakingTransactionType.Withdraw -> resourceReference(R.string.staking_withdraw)
|
||||
is TransactionType.TronStakingTransactionType.ClaimRewards -> resourceReference(R.string.common_claim_rewards)
|
||||
is TransactionType.TronStakingTransactionType.Withdraw -> { resourceReference(R.string.staking_withdraw) }
|
||||
is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation)
|
||||
}
|
||||
|
||||
|
|
@ -96,9 +99,13 @@ internal class TxHistoryItemStateConverter(
|
|||
},
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
is InteractionAddressType.Staking -> resourceReference(
|
||||
id = R.string.common_staking,
|
||||
is InteractionAddressType.Validator -> resourceReference(
|
||||
id = R.string.transaction_history_transaction_validator,
|
||||
formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()),
|
||||
)
|
||||
null -> {
|
||||
TextReference.EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractDirection() =
|
||||
|
|
@ -111,7 +118,8 @@ internal class TxHistoryItemStateConverter(
|
|||
}
|
||||
|
||||
private fun TxHistoryItem.getAmount(): String {
|
||||
if (type == TransactionType.TronStakingTransactionType.Vote ||
|
||||
if (type is TransactionType.TronStakingTransactionType.Vote ||
|
||||
type == TransactionType.TronStakingTransactionType.ClaimRewards ||
|
||||
type == TransactionType.TronStakingTransactionType.Withdraw
|
||||
) {
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideIn
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
|
@ -19,7 +19,6 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -46,7 +45,8 @@ import androidx.compose.ui.unit.IntOffset
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.atoms.Hand
|
||||
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
|
|
@ -54,6 +54,7 @@ import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBot
|
|||
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
|
||||
import com.tangem.core.ui.components.rememberIsKeyboardVisible
|
||||
import com.tangem.core.ui.components.sheetscaffold.*
|
||||
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
|
||||
import com.tangem.core.ui.components.snackbar.TangemSnackbar
|
||||
|
|
@ -425,8 +426,17 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
modifier = Modifier.sizeIn(maxHeight = maxHeight - statusBarHeight),
|
||||
) {
|
||||
Hand(Modifier.drawBehind { drawRect(backgroundColor.value) })
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
// expand bottom sheet when clicked on the header
|
||||
.clickable(
|
||||
enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded,
|
||||
indication = null,
|
||||
interactionSource = null,
|
||||
) {
|
||||
coroutineScope.launch { bottomSheetState.expand() }
|
||||
}
|
||||
.onFocusChanged {
|
||||
isSearchFieldFocused = it.isFocused
|
||||
},
|
||||
|
|
@ -468,7 +478,11 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
}
|
||||
|
||||
BottomSheetScrim(
|
||||
color = BottomSheetDefaults.ScrimColor,
|
||||
color = if (state.showMarketsOnboarding) {
|
||||
Color.Black.copy(alpha = .65f)
|
||||
} else {
|
||||
BottomSheetDefaults.ScrimColor
|
||||
},
|
||||
visible = bottomSheetState.targetValue == TangemSheetValue.Expanded ||
|
||||
state.showMarketsOnboarding,
|
||||
onDismissRequest = {
|
||||
|
|
@ -575,7 +589,7 @@ internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) {
|
|||
|
||||
@Composable
|
||||
private fun MarketsTooltipContent(modifier: Modifier = Modifier) {
|
||||
val backgroundColor = TangemTheme.colors.background.primary
|
||||
val backgroundColor = TangemTheme.colors.background.action
|
||||
val cornerRadius = CornerRadius(x = 14.dp.toPx())
|
||||
val tipDpSize = DpSize(width = 20.dp, height = 8.dp)
|
||||
|
||||
|
|
|
|||
|
|
@ -89,9 +89,9 @@ markdownComposeView = "0.5.4"
|
|||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
tangemBlockchainSdk = "develop-793"
|
||||
tangemBlockchainSdk = "develop-797"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "develop-381"
|
||||
tangemCardSdk = "develop-385"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
tangemVico = "2.0.0-alpha.25-tangem16"
|
||||
#tangemVico = "0.0.1" # Keep it! - used for local builds ^
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue