Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-04 20:46:39 +03:00
commit 709bbdbe43
68 changed files with 894 additions and 441 deletions

View file

@ -3,12 +3,12 @@ package com.tangem.tap.common.url
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK
import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT
import androidx.core.net.toUri
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.getColorCompat
@ -25,30 +25,37 @@ internal class CustomTabsUrlOpener : UrlOpener {
}
}
override fun openUrlExternalBrowser(url: String) {
foregroundActivityObserver.withForegroundActivity { context ->
if (url.isEmpty()) return@withForegroundActivity
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
context.startActivity(browserIntent)
}
}
private fun openUrl(url: String, context: Context) {
if (url.isEmpty()) return
val customTabsIntent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setNavigationBarColor(context.getColorCompat(R.color.toolbarColor))
.build(),
)
.setColorScheme(
if (MutableAppThemeModeHolder.isDarkThemeActive) COLOR_SCHEME_DARK else COLOR_SCHEME_LIGHT,
)
.build()
customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri())
runCatching {
if (checkCustomTabsAvailability(context, browserIntent)) {
context.startActivity(browserIntent)
} else {
customTabsIntent.launchUrl(context, Uri.parse(url))
val customTabsIntent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setNavigationBarColor(context.getColorCompat(R.color.toolbarColor))
.build(),
)
.setColorScheme(
if (MutableAppThemeModeHolder.isDarkThemeActive) COLOR_SCHEME_DARK else COLOR_SCHEME_LIGHT,
)
.build()
customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
customTabsIntent.launchUrl(context, url.toUri())
}
}.onFailure {
Timber.e(it.message)
Timber.e(it)
}
}

View file

@ -21,7 +21,7 @@
},
{
"name": "WALLET_CONNECT_REDESIGN_ENABLED",
"version": "undefined"
"version": "5.27.0"
},
{
"name": "PUSH_NOTIFICATIONS_ENABLED",

View file

@ -15,8 +15,8 @@ data class AssetDiff(
data class Asset(
@Json(name = "chain_id") val chainId: Int? = null,
@Json(name = "logo_url") val logoUrl: String? = null,
@Json(name = "symbol") val symbol: String,
@Json(name = "decimals") val decimals: Int,
@Json(name = "symbol") val symbol: String? = null,
@Json(name = "decimals") val decimals: Int? = null,
)
@JsonClass(generateAdapter = true)

View file

@ -18,6 +18,8 @@ data class SpenderDetails(
@JsonClass(generateAdapter = true)
data class ExposureDetail(
@Json(name = "value") val value: String,
@Json(name = "raw_value") val rawValue: String,
@Json(name = "value") val value: String? = null,
@Json(name = "raw_value") val rawValue: String? = null,
@Json(name = "token_id") val tokenId: String? = null,
@Json(name = "logo_url") val logoUrl: String? = null,
)

View file

@ -5,4 +5,8 @@ class DummyUrlOpener : UrlOpener {
override fun openUrl(url: String) {
/* no-op */
}
override fun openUrlExternalBrowser(url: String) {
/* no-op */
}
}

View file

@ -3,4 +3,6 @@ package com.tangem.core.navigation.url
interface UrlOpener {
fun openUrl(url: String)
fun openUrlExternalBrowser(url: String)
}

View file

@ -66,6 +66,10 @@
<item quantity="one">%d carte</item>
<item quantity="other">%d cartes</item>
</plurals>
<plurals name="card_label_token_count">
<item quantity="one">%s token</item>
<item quantity="other">%s tokens</item>
</plurals>
<string name="card_settings_access_code_recovery_disabled_description">Désactivez cette option si vous ne voulez pas que cette carte soit utilisée pour réinitialiser les codes d\'accès sur d\'autres cartes de ce portefeuille. Veuillez noter que cela vous empêchera également de réinitialiser le code d\'accès sur cette carte.</string>
<string name="card_settings_access_code_recovery_enabled_description">Vous permet d\'utiliser cette carte pour réinitialiser le code d\'accès sur d\'autres cartes de ce portefeuille</string>
<string name="card_settings_access_code_recovery_title">Récupération du code d\'accès</string>
@ -111,6 +115,7 @@
<string name="common_cancel">Annuler</string>
<string name="common_change">Changez</string>
<string name="common_choose_action">Choisissez une action</string>
<string name="common_choose_network">Choisissez le réseau</string>
<string name="common_choose_token">Choisir le jeton</string>
<string name="common_claim">Réclamer</string>
<string name="common_claim_rewards">Réclamez des récompenses</string>
@ -152,6 +157,7 @@
<string name="common_generate_addresses">Synchroniser les adresses</string>
<string name="common_go_to_provider">Aller au fournisseur</string>
<string name="common_go_to_token">Aller au jeton</string>
<string name="common_got_it">Compris</string>
<string name="common_hide">Cacher</string>
<string name="common_hour">heure</string>
<string name="common_import">Importez</string>
@ -196,6 +202,7 @@
<string name="common_server_unavailable">Le serveur n\'est pas disponible, veuillez réessayer plus tard</string>
<string name="common_share">Partager</string>
<string name="common_share_link">Partager le lien</string>
<string name="common_show_less">Afficher moins</string>
<string name="common_show_more">Afficher plus</string>
<string name="common_sign">Signez</string>
<string name="common_sign_and_send">Signez et envoyez</string>
@ -205,6 +212,7 @@
<string name="common_submit">Soumettre</string>
<string name="common_success">Avec succès</string>
<string name="common_support">Support</string>
<string name="common_supported_networks">Réseaux pris en charge</string>
<string name="common_swap">Échanger</string>
<string name="common_terms_and_conditions">termes et conditions</string>
<string name="common_terms_of_use">Conditions d\'utilisation</string>
@ -265,6 +273,7 @@
<string name="details_row_subtitle_signed_hashes_format">%s hashes</string>
<string name="details_row_title_cid">ID de l\'appareil</string>
<string name="details_row_title_contact_to_support">Contactez l\'équipe de support</string>
<string name="details_row_title_contact_to_support_chat">Ouvrir le chat d\'assistance</string>
<string name="details_row_title_create_backup">Lier plus de cartes</string>
<string name="details_row_title_currency">Monnaie de l\'application</string>
<string name="details_row_title_flip_to_hide">Retourner pour masquer les soldes</string>
@ -281,6 +290,7 @@
<string name="exchange_tokens_unavailable_tokens_header">Ne peut pas être échangé contre %s</string>
<string name="express_by_provider">Fourni par</string>
<string name="express_cex_status_button_title">Statut</string>
<string name="express_choose_provider">Choisir un fournisseur</string>
<string name="express_choose_providers_subtitle">Tangem propose des échanges de jetons via des fournisseurs tiers selon les conditions de chaque fournisseur</string>
<string name="express_choose_providers_title">Fournisseur</string>
<string name="express_error_code">Une erreur s\'est produite. Code : %s</string>
@ -340,6 +350,9 @@
<string name="express_status_hide_button_text">Masquer cette transaction</string>
<string name="express_status_hide_dialog_text">Une fois masqué, le statut de la transaction ne peut plus être consulté. Vous pouvez simplement faire glisser votre doigt pour le fermer.</string>
<string name="express_status_hide_dialog_title">Masquer le statut de la transaction ?</string>
<string name="express_swap_not_supported_text">Ce jeton n\'est pas pris en charge. Veuillez choisir un autre jeton à échanger.</string>
<string name="express_swap_not_supported_title">%s n\'est pas pris en charge</string>
<string name="express_swap_with">Échanger avec</string>
<string name="express_token_list_empty_search">Aucun jeton trouvé. Veuillez essayer une autre demande</string>
<string name="express_transaction_id">ID : %s</string>
<string name="express_transaction_id_copied">ID de transaction copié</string>
@ -367,6 +380,8 @@
<string name="give_permission_swap_subtitle" formatted="false">Pour continuer, accordez aux smart contracts de %1s l\'autorisation d\'utiliser votre %2s</string>
<string name="give_permission_title">Donner l\'autorisation</string>
<string name="give_permission_unlimited">Illimité</string>
<string name="home_button_add_existing_wallet">Ajouter un Portefeuille existant</string>
<string name="home_button_create_new_wallet">Créer un nouveau Portefeuille</string>
<string name="home_button_order">Commandez</string>
<string name="home_button_scan">Scannez</string>
<string name="hot_crypto_add_token_subtitle">à %s</string>
@ -423,6 +438,7 @@
<string name="markets_add_to_my_portfolio_unavailable_description">Cet actif n\'est pas pris en charge dans le portefeuille</string>
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">Cet actif n\'est pas disponible pour ce portefeuille</string>
<string name="markets_add_token">Ajoutez un jeton</string>
<string name="markets_apy_placeholder">APY %s</string>
<string name="markets_available_networks">Réseaux disponibles</string>
<string name="markets_common_my_portfolio">Mon portfolio</string>
<string name="markets_common_title">Marché</string>
@ -709,6 +725,7 @@
<string name="push_notifications_permission_alert_positive_button">Paramètres</string>
<string name="push_notifications_permission_alert_title">Activer les notifications</string>
<string name="push_transactions_notifications_description">Recevez des alertes pour les transactions entrantes sur les réseaux pris en charge.</string>
<string name="push_transactions_notifications_title">Notifications de transaction</string>
<string name="qr_scanner_camera_denied_gallery_button">Sélectionnez dans la galerie</string>
<string name="qr_scanner_camera_denied_settings_button">Paramètres</string>
<string name="qr_scanner_camera_denied_text">Vous n\'avez pas donné accès à votre caméra</string>
@ -818,6 +835,9 @@
<string name="send_memo">Mémo : %s</string>
<string name="send_memo_destination_tag_error">Mémo invalide</string>
<string name="send_network_fee_warning_title">Couverture des frais de réseau</string>
<string name="send_nonce">Nonce</string>
<string name="send_nonce_footer">Numéro unique pour chaque transaction. Utilisez-le pour renvoyer ou annuler une transaction en attente.</string>
<string name="send_nonce_hint">Entrez le nonce…</string>
<string name="send_notification_exceed_balance_text">Fonds insuffisants pour le transfert, car le total des frais et du montant du transfert dépasse le solde existant</string>
<string name="send_notification_exceed_balance_title">Le total dépasse le solde</string>
<string name="send_notification_existential_deposit_text">Un solde d\'au moins %s est requis pour conserver votre compte sur la blockchain afin d\'éviter les risques de sécurité. Ce montant restera sur votre solde et ne pourra pas être retiré.</string>
@ -847,6 +867,7 @@
<string name="send_recipient_address_error">Adresse non valide</string>
<string name="send_recipient_address_footer">Assurez-vous que l\'adresse du portefeuille de réception est sur le réseau %s pour éviter de perdre vos jetons</string>
<string name="send_recipient_address_footer_highlighted_part">envoyer à %s</string>
<string name="send_recipient_address_footer_v2">Assurez-vous de %s une adresse réseau, car des erreurs peuvent entraîner des transferts perdus</string>
<string name="send_recipient_label">Envoyer à</string>
<string name="send_recipient_memo_footer">Un Memo/Destination Tag est un identifiant unique permettant de différencier les transactions envoyées au même destinataire sur le même réseau. Attention : L\'omission d\'un mémo peut entraîner des fonds mal placés</string>
<string name="send_recipient_memo_footer_v2">Le Memo / Destination Tag est un code qui distingue les transactions envoyées à un destinataire partagé sur un réseau crypto.</string>
@ -873,7 +894,11 @@
<string name="send_validation_invalid_fee">Les frais de commissions dépassent le solde</string>
<string name="send_validation_invalid_total">Le total dépasse le solde</string>
<string name="send_with_swap_confirm_title">Échanger et envoyer</string>
<string name="send_with_swap_notification_text">Envoyez n\'importe quel jeton et nous le convertirons en cours de route. Votre destinataire reçoit exactement ce dont il a besoin, en toute simplicité.</string>
<string name="send_with_swap_recipient_amount_success_title">Le destinataire recevra</string>
<string name="send_with_swap_recipient_amount_text">Un destinataire sera envoyé</string>
<string name="send_with_swap_recipient_amount_title">Montant à recevoir</string>
<string name="send_with_swap_title">Envoyer avec swap</string>
<string name="sent_transaction_sent_title">Transaction envoyée</string>
<string name="settings_card_settings_footer">Scannez la carte/ bague que vous souhaitez configurer.</string>
<string name="settings_forget_wallet">Oublier le portefeuille</string>
@ -1014,6 +1039,7 @@
<string name="story_web3_title">Compatible avec Web 3.0</string>
<string name="sui_not_enough_coin_for_fee_description">Une transaction entrante d\'au moins de %1$s est requise pour continuer</string>
<string name="sui_not_enough_coin_for_fee_title">Fonds insuffisants</string>
<string name="swap_fixed_rate">Taux fixe</string>
<string name="swap_give_permission_fee_footer">Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange.</string>
<string name="swap_promo_text">Échangez plus de jetons à de meilleurs taux directement dans votre portefeuille.</string>
<string name="swap_promo_title">Nouveau fournisseur d\'échange disponible !</string>
@ -1091,6 +1117,8 @@
<string name="transaction_notifications_warning_title">Notifications de transaction</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Le montant minimum pour effectuer cette transaction est %1$s.</string>
<string name="tron_will_be_send_token_fee_description">Les frais de réseau Tron pour les jetons populaires peuvent être plus élevés. Le staking de TRX peut contribuer à réduire les coûts de transaction.</string>
<string name="tron_will_be_send_token_fee_title">Économisez sur les frais de réseau Tron</string>
<string name="try_to_load_data_again_button_title">Réessayez</string>
<string name="twin_error_same_card">Vous avez scanné la même carte. Pour créer un portefeuille jumeau, vous devez scanner la carte portant le numéro %d</string>
<string name="twin_error_wrong_twin">Vous avez scanné une mauvaise carte jumelle. S\'il vous plaît, essayez-en un autre</string>
@ -1103,6 +1131,8 @@
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="twins_recreate_warning">Cette action est irréversible. Vous n\'aurez plus accès à l\'ancien portefeuille.</string>
<string name="twins_scan_twin_with_number">Appuyez sur la carte jumelle avec le numéro %s et ne la retirez pas jusqu\'à la fin de l\'opération</string>
<string name="unexpected_error_description">Veuillez réessayer ultérieurement. Si le problème persiste, veuillez contacter le support.</string>
<string name="unexpected_error_title">Une erreur s\'est produite !</string>
<string name="universal_error">Nous avons rencontré une erreur. Code d\'erreur : %s. Veuillez contacter notre équipe de support.</string>
<string name="unlock_wallet_description_full">Utilisez %s ou scannez une carte/bague pour avoir accès à votre portefeuille</string>
<string name="unsupported_wc_version">Échec de la connexion : Cette dApp utilise Wallet Connect version1.0, qui n\'est pas prise en charge. Veuillez vous assurer que la dApp prend en charge Wallet Connect version2.0 pour réussir la connexion.</string>
@ -1253,6 +1283,7 @@
<string name="warning_rate_app_message">Votre avis nous motive à améliorer encore le Portefeuille Tangem</string>
<string name="warning_rate_app_title">Vous appréciez Tangem ?</string>
<string name="warning_receive_blocked_hedera_token_association_required_message">Vous devez associer votre jeton avant de recevoir des jetons</string>
<string name="warning_receive_blocked_token_trustline_required_message">Vous devez ouvrir la ligne de confiance pour votre jeton avant de le recevoir</string>
<string name="warning_rent_fee_title">Frais de location de réseau requis</string>
<string name="warning_seedphrase_action_required_title">Action requise</string>
<string name="warning_seedphrase_contacted_support">Avez-vous contacté l\'assistance via l\'application ou par e-mail dans les 7 jours suivant la création d\'un portefeuille ? Si vous l\'avez fait ou si vous n\'êtes pas sûr, suivez et complétez les instructions.</string>
@ -1273,17 +1304,38 @@
<string name="warning_testnet_card_title">À des fins de test uniquement</string>
<string name="warning_token_balance_not_updated">Le solde peut être obsolète. Rafraîchissez la page.</string>
<string name="warning_token_required_min_coin_reserve">Pas assez de %1$s. Rechargez votre compte %2$s pour associer ce jeton.</string>
<string name="warning_token_trustline_button_title">Activer Trustline</string>
<string name="warning_token_trustline_subtitle">Une Trustline doit être activée pour recevoir ce jeton. Le réseau requiert une réserve de %1$s %2$s</string>
<string name="warning_token_trustline_title">Trustline requise</string>
<string name="wc_alert_audit_malicious_domain">Domaine malveillant</string>
<string name="wc_alert_sign_anyway">Signer quand même</string>
<string name="wc_alert_unknown_error_description_no_error_code">Si le problème persiste, nhésitez pas à contacter notre support.</string>
<string name="wc_alert_unsupported_dapps_description">Le portefeuille Tangem ne prend actuellement pas en charge %ss</string>
<string name="wc_alert_unsupported_dapps_title">dApp non prise en charge</string>
<string name="wc_alert_unsupported_method_description">Code d\'erreur : 8 005. Si le problème persiste, n\'hésitez pas à contacter notre support.</string>
<string name="wc_alert_unsupported_method_title">Nous avons rencontré une erreur inconnue</string>
<string name="wc_allow_to_spend">Autoriser à dépenser</string>
<string name="wc_common_address">Adresse</string>
<string name="wc_common_loading">Chargement</string>
<string name="wc_common_unlimited">Illimité</string>
<string name="wc_connections">Connexions</string>
<string name="wc_contents">Contenu</string>
<string name="wc_copy_data_button_text">Copier les données</string>
<string name="wc_disconnect_all">Déconnecter tout</string>
<string name="wc_disconnect_all_alert_desc">Texte sur la déconnexion de toutes les dApps</string>
<string name="wc_disconnect_all_alert_title">Déconnecter toutes les dApps</string>
<string name="wc_estimated_wallet_changes">Modifications estimées du portefeuille</string>
<string name="wc_estimated_wallet_changes_not_simulated">La transaction n\'a pas pu être simulée. Veuillez procéder avec prudence.</string>
<string name="wc_malicious_transaction">Transaction malveillante</string>
<string name="wc_new_connection">Nouvelle connexion</string>
<string name="wc_no_sessions_desc">Connectez votre portefeuille à différentes dApps</string>
<string name="wc_no_sessions_title">Aucune séance</string>
<string name="wc_request_from">Demande de</string>
<string name="wc_signature_type">Type de signature</string>
<string name="wc_transaction_info_to_title">À</string>
<string name="wc_transaction_request">Demande de transaction</string>
<string name="wc_transaction_request_title">Demande de transaction</string>
<string name="wc_unlimited_amount">Montant illimité</string>
<string name="welcome_interrupted_backup_alert_discard">Ignorer</string>
<string name="welcome_interrupted_backup_alert_message">Vous avez une sauvegarde interrompue. Voulez-vous la reprendre ?</string>
<string name="welcome_interrupted_backup_alert_resume">Oui, reprendre</string>

View file

@ -12,6 +12,7 @@
<string name="access_code_create_description">Set a %s-digit Access Code to unlock your wallet.</string>
<string name="access_code_create_title">Create Access Code</string>
<string name="access_code_navtitle">Access code</string>
<string name="account_form_name">Account name</string>
<string name="action_buttons_buy_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for purchase</string>
<string name="action_buttons_sell_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for selling.</string>
<string name="action_buttons_sell_navigation_bar_title">Sell</string>
@ -1500,6 +1501,7 @@
<string name="wc_new_connection">New connection</string>
<string name="wc_no_sessions_desc">Connect your wallet to a different dApps</string>
<string name="wc_no_sessions_title">No sessions</string>
<string name="wc_no_wallet_changes_detected">No wallet changes detected</string>
<string name="wc_notification_security_risk_subtitle">This domain is flagged as unsafe by multiple security providers. Leave immediately to protect your assets</string>
<string name="wc_notification_security_risk_title">Known security risk</string>
<string name="wc_request_from">Request from</string>
@ -1511,6 +1513,7 @@
<string name="wc_uri_already_used_description">Ensure that each pairing attempt uses a fresh and unique URI</string>
<string name="wc_uri_already_used_title">URI already used</string>
<string name="wc_wallet_connect">Wallet connect</string>
<string name="wc_warning_transaction">Suspicious transaction</string>
<string name="welcome_interrupted_backup_alert_discard">Discard</string>
<string name="welcome_interrupted_backup_alert_message">You have an interrupted backup. Do you want to resume?</string>
<string name="welcome_interrupted_backup_alert_resume">Yes, resume</string>

View file

@ -214,12 +214,15 @@ internal fun TextsBlock(
SpacerH(height = TangemTheme.dimens.spacing2)
}
Text(
text = subtitle.resolveReference(),
color = subtitleColor,
style = TangemTheme.typography.caption2,
modifier = Modifier.testTag(NotificationTestTags.TEXT),
)
val subtitleText = subtitle.resolveReference()
if (subtitleText.isNotEmpty()) {
Text(
text = subtitleText,
color = subtitleColor,
style = TangemTheme.typography.caption2,
modifier = Modifier.testTag(NotificationTestTags.TEXT),
)
}
}
}

View file

@ -22,9 +22,15 @@ import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.coroutines.launch
@Composable
fun TangemTooltip(text: String, content: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier) {
fun TangemTooltip(
text: String,
content: @Composable (Modifier) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
InternalTangemTooltip(
modifier = modifier,
enabled = enabled,
tooltipContent = {
Text(
modifier = Modifier.background(TangemTheme.colors.icon.secondary),
@ -38,9 +44,15 @@ fun TangemTooltip(text: String, content: @Composable (Modifier) -> Unit, modifie
}
@Composable
fun TangemTooltip(text: AnnotatedString, content: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier) {
fun TangemTooltip(
text: AnnotatedString,
content: @Composable (Modifier) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
InternalTangemTooltip(
modifier = modifier,
enabled = enabled,
tooltipContent = {
Text(
modifier = Modifier.background(TangemTheme.colors.icon.secondary),
@ -59,6 +71,7 @@ private fun InternalTangemTooltip(
tooltipContent: @Composable () -> Unit,
content: @Composable (Modifier) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
val tooltipState = rememberTooltipState(isPersistent = true)
val coroutineScope = rememberCoroutineScope()
@ -78,11 +91,14 @@ private fun InternalTangemTooltip(
)
},
content = {
content(
val contentModifier = if (enabled) {
Modifier.clickableSingle(
onClick = { coroutineScope.launch { tooltipState.show() } },
),
)
)
} else {
Modifier
}
content(contentModifier)
},
)
}

View file

@ -16,6 +16,7 @@ import org.json.JSONArray
private const val SUCCESS_STATUS = "Success"
private const val DOMAIN_CHECKED_STATUS = "hit"
private const val VALIDATION_SAFE_STATUS = "Benign"
private const val VALIDATION_WARNING_STATUS = "Warning"
internal object BlockAidMapper {
@ -32,6 +33,7 @@ internal object BlockAidMapper {
validation = when {
from.validation.status != SUCCESS_STATUS -> ValidationResult.FAILED_TO_VALIDATE
from.validation.resultType == VALIDATION_SAFE_STATUS -> ValidationResult.SAFE
from.validation.resultType == VALIDATION_WARNING_STATUS -> ValidationResult.WARNING
else -> ValidationResult.UNSAFE
},
simulation = if (from.simulation.status != SUCCESS_STATUS) {
@ -83,7 +85,7 @@ internal object BlockAidMapper {
from.exposures,
)
!from.traces.isNullOrEmpty() -> mapNftSendReceiveTransaction(from.traces)
else -> SimulationResult.FailedToSimulate
else -> SimulationResult.Success(data = SimulationData.NoWalletChangesDetected)
}
}
@ -92,15 +94,15 @@ internal object BlockAidMapper {
val tokenInfo = TokenInfo(
chainId = exposure.asset.chainId,
logoUrl = exposure.asset.logoUrl,
symbol = exposure.asset.symbol,
decimals = exposure.asset.decimals,
symbol = exposure.asset.symbol ?: "",
decimals = exposure.asset.decimals ?: 0,
)
exposure.spenders.flatMap { (_, spender) ->
val isUnlimited = spender.isApprovedForAll == true
val approval = spender.approval?.hexToBigDecimal()
spender.exposure.mapNotNull { detail ->
spender.exposure.map { detail ->
ApprovedAmount(
approvedAmount = detail.value.toBigDecimalOrNull() ?: approval ?: return@mapNotNull null,
approvedAmount = detail.value?.toBigDecimalOrNull() ?: approval ?: 1.toBigDecimal(),
isUnlimited = isUnlimited,
tokenInfo = tokenInfo,
)
@ -110,7 +112,7 @@ internal object BlockAidMapper {
return if (!amounts.isNullOrEmpty()) {
SimulationResult.Success(SimulationData.Approve(amounts))
} else {
SimulationResult.FailedToSimulate
SimulationResult.Success(SimulationData.NoWalletChangesDetected)
}
}
@ -122,8 +124,8 @@ internal object BlockAidMapper {
val token = TokenInfo(
chainId = diff.asset.chainId,
logoUrl = diff.asset.logoUrl,
symbol = diff.asset.symbol,
decimals = diff.asset.decimals,
symbol = diff.asset.symbol ?: "",
decimals = diff.asset.decimals ?: 0,
)
diff.outTransfer.orEmpty().forEach { transfer ->
transfer.value?.toBigDecimalOrNull()?.let { amount ->
@ -140,7 +142,7 @@ internal object BlockAidMapper {
return if (sendInfo.isNotEmpty() || receiveInfo.isNotEmpty()) {
SimulationResult.Success(SimulationData.SendAndReceive(send = sendInfo, receive = receiveInfo))
} else {
SimulationResult.FailedToSimulate
SimulationResult.Success(SimulationData.NoWalletChangesDetected)
}
}
@ -154,7 +156,7 @@ internal object BlockAidMapper {
return if (!sendInfo.isNullOrEmpty()) {
SimulationResult.Success(SimulationData.SendAndReceive(send = sendInfo, receive = listOf()))
} else {
SimulationResult.FailedToSimulate
SimulationResult.Success(SimulationData.NoWalletChangesDetected)
}
}
}

View file

@ -114,7 +114,7 @@ class BlockAidMapperTest {
val result = mapper.mapToDomain(response)
Truth.assertThat(result.validation).isEqualTo(ValidationResult.FAILED_TO_VALIDATE)
Truth.assertThat(result.simulation is SimulationResult.FailedToSimulate).isTrue()
Truth.assertThat(result.simulation is SimulationResult.Success).isTrue()
}
@Test
@ -160,6 +160,6 @@ class BlockAidMapperTest {
)
val result = mapper.mapToDomain(txResponse)
Truth.assertThat(result.simulation is SimulationResult.FailedToSimulate).isTrue()
Truth.assertThat(result.simulation is SimulationResult.Success).isTrue()
}
}

View file

@ -225,8 +225,12 @@ internal object WalletConnectDataModule {
@Provides
@Singleton
fun wcRequestUseCaseFactory(diHelperBox: DiHelperBox): WcRequestUseCaseFactory {
return DefaultWcRequestUseCaseFactory(diHelperBox.handlers)
fun wcRequestUseCaseFactory(
diHelperBox: DiHelperBox,
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
analytics: AnalyticsEventHandler,
): WcRequestUseCaseFactory {
return DefaultWcRequestUseCaseFactory(diHelperBox.handlers, namespaceConverters, analytics)
}
@Provides

View file

@ -7,7 +7,6 @@ import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.model.NamespaceKey
import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter
@ -15,7 +14,6 @@ import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Compani
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.walletconnect.model.*
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
@ -156,16 +154,6 @@ internal class WcEthNetwork(
val ethChainId = chainId.reference.toIntOrNull() ?: return null
return Blockchain.fromChainId(ethChainId)
}
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = network.toBlockchain()
if (!blockchain.isEvm()) return null
val chainId = blockchain.getChainId() ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId.toString(),
)
}
}
internal class Factories @Inject constructor(

View file

@ -41,6 +41,9 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
WcMutableFee {
private var approvalAmount: WcApprovedAmount? = null
// in case of change TransactionExtras
// change approvalAmount for example
private var isIgnoreDAppFee: Boolean = false
private var dAppFee: Fee? = null
override val securityStatus: LceFlow<Throwable, BlockAidTransactionCheck.Result> =
@ -102,7 +105,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
},
)
approvalAmount = action.amount
dAppFee = null
isIgnoreDAppFee = true
uncompiled.copy(extras = extras.copy(callData = callData))
}
is WcEthTxAction.UpdateFee -> uncompiled.copy(fee = action.fee)
@ -111,8 +114,11 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
emit(newState)
}
override fun dAppFee(): Fee? {
return dAppFee
override suspend fun dAppFee(): Fee? {
if (isIgnoreDAppFee) return null
if (dAppFee != null) return dAppFee
return ethTxHelper.getDAppFee(method.transaction, wallet, network)
.also { dAppFee = it }
}
override fun updateFee(fee: Fee) {
@ -120,9 +126,8 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
}
override fun invoke(): Flow<WcSignState<TransactionData>> = flow {
dAppFee = ethTxHelper.getDAppFee(method.transaction, wallet, network)
val transactionData = ethTxHelper.createTransactionData(
dAppFee = dAppFee,
dAppFee = dAppFee(),
network = context.network,
txParams = method.transaction,
) ?: return@flow

View file

@ -41,6 +41,9 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
WcMutableFee {
private var approvalAmount: WcApprovedAmount? = null
// in case of change TransactionExtras
// change approvalAmount for example
private var isIgnoreDAppFee: Boolean = false
private var dAppFee: Fee? = null
override val securityStatus = blockAidDelegate.getSecurityStatus(
@ -101,7 +104,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
},
)
approvalAmount = action.amount
dAppFee = null
isIgnoreDAppFee = true
uncompiled.copy(extras = extras.copy(callData = callData))
}
is WcEthTxAction.UpdateFee -> uncompiled.copy(fee = action.fee)
@ -115,17 +118,19 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
}
override fun invoke(): Flow<WcSignState<TransactionData>> = flow {
dAppFee = ethTxHelper.getDAppFee(method.transaction, wallet, network)
val transactionData = ethTxHelper.createTransactionData(
dAppFee = dAppFee,
dAppFee = dAppFee(),
network = context.network,
txParams = method.transaction,
) ?: return@flow
emitAll(delegate.invoke(transactionData))
}
override fun dAppFee(): Fee? {
return dAppFee
override suspend fun dAppFee(): Fee? {
if (isIgnoreDAppFee) return null
if (dAppFee != null) return dAppFee
return ethTxHelper.getDAppFee(method.transaction, wallet, network)
.also { dAppFee = it }
}
override fun getAmount(): WcApprovedAmount? {

View file

@ -76,7 +76,7 @@ internal class WcEthTxHelper @Inject constructor(
?: return null
val approves = (simulation.data as? SimulationData.Approve)?.approvedAmounts
?: return null
if (approves.size != 1) return null
if (approves.isEmpty()) return null
val amount = approves.first()
return amount
}

View file

@ -8,7 +8,6 @@ import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.extensions.decodeBase58
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.extensions.toHexString
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.model.NamespaceKey
@ -17,7 +16,6 @@ import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Compani
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.HandleMethodError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.model.WcSolanaMethodName
@ -88,27 +86,14 @@ internal class WcSolanaNetwork(
override val namespaceKey: NamespaceKey = NamespaceKey("solana")
override fun toBlockchain(chainId: CAIP2): Blockchain? {
val isMainNet = MAINNET_CHAIN_ID.any { it.lowercase() == chainId.reference.lowercase() }
if (chainId.namespace != namespaceKey.key) return null
return when (chainId.reference) {
MAINNET_CHAIN_ID -> Blockchain.Solana
TESTNET_CHAIN_ID -> Blockchain.SolanaTestnet
return when {
isMainNet -> Blockchain.Solana
chainId.reference.lowercase() == TESTNET_CHAIN_ID -> Blockchain.SolanaTestnet
else -> null
}
}
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = network.toBlockchain()
val chainId = when (blockchain) {
Blockchain.Solana -> MAINNET_CHAIN_ID
Blockchain.SolanaTestnet -> TESTNET_CHAIN_ID
else -> null
}
chainId ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId,
)
}
}
private fun WcSolanaMethodName.toMethod(request: WcSdkSessionRequest): Either<Throwable, WcSolanaMethod?> {
@ -140,7 +125,7 @@ internal class WcSolanaNetwork(
)
companion object {
private const val MAINNET_CHAIN_ID = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
private val MAINNET_CHAIN_ID = listOf("5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ")
private const val TESTNET_CHAIN_ID = "4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z"
}
}

View file

@ -110,8 +110,10 @@ internal class AssociateNetworksDelegate(
.distinctBy { it.rawId }
}
private fun Map<String, Namespace.Proposal>.setOfChainId(): Set<String> =
this.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()
private fun missingNetworkName(chainId: String): String = chainId.replaceFirstChar(Char::titlecase)
companion object {
internal fun Map<String, Namespace.Proposal>.setOfChainId(): Set<String> =
this.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.data.walletconnect.pair
import com.reown.walletkit.client.Wallet
import com.tangem.data.walletconnect.model.CAIP10
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate.Companion.setOfChainId
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.domain.models.network.Network
@ -22,6 +24,26 @@ internal class CaipNamespaceDelegate(
val userWallet = sessionForApprove.wallet
val result = mutableMapOf<String, Session>()
val requiredNamespaces = sessionProposal.requiredNamespaces.setOfChainId()
val optionalNamespaces = sessionProposal.optionalNamespaces.setOfChainId()
val allWcNetworks = (requiredNamespaces + optionalNamespaces)
.mapNotNull { chainId ->
val network = namespaceConverters
.firstNotNullOfOrNull { it.toNetwork(chainId, userWallet) }
?: return@mapNotNull null
val caip2 = CAIP2.fromRaw(chainId) ?: return@mapNotNull null
network to caip2
}
suspend fun createCAIP10(userWalletId: UserWalletId, network: Network): CAIP10? {
val address = walletManagersFacade.getDefaultAddress(userWalletId, network)
val chainId = allWcNetworks
.find { (wcNetwork, _) -> network.rawId == wcNetwork.rawId }
?.second
if (chainId == null || address == null) return null
return CAIP10(chainId = chainId, accountAddress = address)
}
wcNetworksConverter.convertNetworksForApprove(sessionForApprove)
.mapNotNull { createCAIP10(userWallet.walletId, it) }
.forEach { account ->
@ -52,13 +74,6 @@ internal class CaipNamespaceDelegate(
}
}
private suspend fun createCAIP10(userWalletId: UserWalletId, network: Network): CAIP10? {
val address = walletManagersFacade.getDefaultAddress(userWalletId, network)
val chainId = namespaceConverters.firstNotNullOfOrNull { it.toCAIP2(network) }
if (chainId == null || address == null) return null
return CAIP10(chainId = chainId, accountAddress = address)
}
private data class Session(
val chains: MutableSet<String> = mutableSetOf(),
val accounts: MutableSet<String> = mutableSetOf(),

View file

@ -49,7 +49,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
val pairResult = sdkDelegate.pair(uri)
.onLeft {
Timber.tag(WC_TAG).e(it, "Failed to call pair $pairRequest")
analytics.send(WcAnalyticEvents.PairFailed)
analytics.send(WcAnalyticEvents.PairFailed(it.code))
emit(WcPairState.Error(it))
}
.getOrNull() ?: return@flow
@ -65,7 +65,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
val proposalState = buildProposalState(sdkSessionProposal, sdkVerifyContext)
.onLeft {
analytics.send(WcAnalyticEvents.PairFailed)
analytics.send(WcAnalyticEvents.PairFailed(it.code))
emit(WcPairState.Error(it))
}
.getOrNull() ?: return@flow
@ -115,6 +115,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
}.onCompletion {
if (it != null) {
Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest")
emit(WcPairState.Error(WcPairError.Unknown(it.message.orEmpty())))
} else {
Timber.tag(WC_TAG).i("Completed successfully $pairRequest")
}
@ -134,7 +135,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
private suspend fun walletKitApproveSession(
sessionForApprove: WcSessionApprove,
sdkSessionProposal: Wallet.Model.SessionProposal,
): Either<WcPairError, Wallet.Model.SettledSessionResponse.Result> {
): Either<WcPairError, Wallet.Model.SettledSessionResponse.Result> = try {
val namespaces = caipNamespaceDelegate.associate(
sdkSessionProposal,
sessionForApprove,
@ -143,7 +144,10 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
proposerPublicKey = sdkSessionProposal.proposerPublicKey,
namespaces = namespaces,
)
return sdkDelegate.approve(sessionApprove)
sdkDelegate.approve(sessionApprove)
} catch (e: Throwable) {
Timber.tag(WC_TAG).e(e, "Failed to sdk approve session $pairRequest")
WcPairError.ApprovalFailed(e.message.orEmpty()).left()
}
private suspend fun buildProposalState(

View file

@ -150,7 +150,7 @@ internal class WcPairSdkDelegate : WcSdkObserver {
private fun Throwable.toApproveError() = WcPairError.ApprovalFailed(this.localizedMessage.orEmpty()).left()
companion object {
private const val CALLBACK_TIMEOUT = 30
private const val CALLBACK_TIMEOUT = 60
// com.reown.android.pairing.engine.domain.PairingEngine.pair
private val pairingExpiredMessages = listOf(
"Pairing URI expired",

View file

@ -3,10 +3,14 @@ package com.tangem.data.walletconnect.request
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
import com.tangem.domain.walletconnect.model.HandleMethodError
import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.WcRequestError.Companion.code
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase
import timber.log.Timber
@ -14,6 +18,8 @@ import javax.inject.Inject
internal class DefaultWcRequestUseCaseFactory @Inject constructor(
private val requestConverters: Set<WcRequestToUseCaseConverter>,
private val namespaceConverters: Set<WcNamespaceConverter>,
private val analytics: AnalyticsEventHandler,
) : WcRequestUseCaseFactory {
@Suppress("UNCHECKED_CAST")
@ -25,12 +31,26 @@ internal class DefaultWcRequestUseCaseFactory @Inject constructor(
?.toUseCase(request)
?: HandleMethodError.UnknownError("Failed to create WcUseCase").left()
return useCase.fold(
val result = useCase.fold(
ifLeft = {
Timber.tag(WC_TAG).e("$it")
it.left()
},
ifRight = { (it as? T)?.right() ?: HandleMethodError.Unsupported(WcMethod.Unsupported(request)).left() },
)
result.onLeft { logError(it, request) }
return result
}
private fun logError(error: HandleMethodError, request: WcSdkSessionRequest) {
val blockchainName = namespaceConverters
.firstNotNullOfOrNull { it.toBlockchain(request.chainId.orEmpty()) }
?.getCoinName().orEmpty()
val event = WcAnalyticEvents.SignatureRequestReceivedFailed(
rawRequest = request,
errorCode = error.code().orEmpty(),
blockchain = blockchainName,
)
analytics.send(event)
}
}

View file

@ -52,7 +52,7 @@ internal class DefaultWcSessionsManager(
if (someMigrated) return@transform // ignore emit, wait next one
}
val associatedSessions: List<WcSession> = associate(inSdk, inStore, wallets)
val someRemove = removeUnknownSessions(inStore, associatedSessions)
val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions)
if (someRemove) return@transform // ignore emit, wait next one
emit(associatedSessions.groupBy { it.wallet })
}
@ -79,7 +79,7 @@ internal class DefaultWcSessionsManager(
override suspend fun removeSession(session: WcSession): Either<Throwable, Unit> {
val topic = session.sdkModel.topic
val sdkCall = sdkDisconnectSession(topic)
.onRight { onSessionDelete.trySend(Wallet.Model.SessionDelete.Success(topic = topic, reason = "")) }
onSessionDelete.trySend(Wallet.Model.SessionDelete.Success(topic = topic, reason = ""))
analytics.send(WcAnalyticEvents.SessionDisconnected(session.sdkModel.appMetaData))
return sdkCall
}
@ -142,10 +142,17 @@ internal class DefaultWcSessionsManager(
return wcSessions
}
private suspend fun removeUnknownSessions(storeSessions: Set<WcSessionDTO>, wcSessions: List<WcSession>): Boolean {
private suspend fun removeUnknownSessions(
storeSessions: Set<WcSessionDTO>,
inSdkSessions: List<Wallet.Model.Session>,
wcSessions: List<WcSession>,
): Boolean {
val unknownStoredSessions = storeSessions
.filterNot { dto -> wcSessions.any { it.sdkModel.topic == dto.topic } }
val haveSomeUnknown = unknownStoredSessions.isNotEmpty()
val unknownSdkSessions = inSdkSessions
.filterNot { sdkSession -> wcSessions.any { it.sdkModel.topic == sdkSession.topic } }
val haveSomeUnknownSdkSessions = unknownSdkSessions.isNotEmpty()
if (haveSomeUnknown) {
Timber.tag(WC_TAG).i("removeUnknownSessions $unknownStoredSessions")
@ -164,7 +171,10 @@ internal class DefaultWcSessionsManager(
store.removeSessions(emptyNetworksDto)
}
if (haveEmptySessions) {
emptyNetworkSessions.map { scope.launch { sdkDisconnectSession(it.sdkModel.topic) } }
emptyNetworkSessions.forEach { scope.launch { sdkDisconnectSession(it.sdkModel.topic) } }
}
if (haveSomeUnknownSdkSessions) {
unknownSdkSessions.forEach { scope.launch { sdkDisconnectSession(it.topic) } }
}
return haveSomeUnknown || haveEmptyDto
}

View file

@ -43,7 +43,6 @@ internal class WcSignUseCaseDelegate<MiddleAction, SignModel>(
val state = MutableStateFlow(WcSignState(initModel, WcSignStep.PreSign))
analytics.send(
WcAnalyticEvents.SignatureRequestReceived(
session = context.session,
rawRequest = context.rawSdkRequest,
network = context.network,
),
@ -73,7 +72,6 @@ internal class WcSignUseCaseDelegate<MiddleAction, SignModel>(
val event = step.result.fold(
ifLeft = { error ->
WcAnalyticEvents.SignatureRequestFailed(
session = context.session,
rawRequest = context.rawSdkRequest,
network = context.network,
errorCode = error.code() ?: error::class.simpleName.orEmpty(),
@ -81,7 +79,6 @@ internal class WcSignUseCaseDelegate<MiddleAction, SignModel>(
},
ifRight = {
WcAnalyticEvents.SignatureRequestHandled(
session = context.session,
rawRequest = context.rawSdkRequest,
network = context.network,
)

View file

@ -16,7 +16,6 @@ internal interface WcNamespaceConverter {
fun toBlockchain(chainId: CAIP2): Blockchain?
fun toBlockchain(chainId: String): Blockchain? = toCAIP2(chainId)?.let { caip2 -> toBlockchain(caip2) }
fun toCAIP2(network: Network): CAIP2?
fun toCAIP2(chainId: String): CAIP2? = CAIP2.fromRaw(chainId)
fun toNetwork(chainId: String, wallet: UserWallet): Network? {

View file

@ -10,6 +10,8 @@ internal object WcSdkSessionRequestConverter : Converter<Wallet.Model.SessionReq
return WcSdkSessionRequest(
topic = value.topic,
chainId = value.chainId,
dAppMetaData = value.peerMetaData?.let { WcAppMetaDataConverter.convert(it) }
?: WcAppMetaDataConverter.empty,
request = JSONRPCRequestConverter.convert(value.request),
)
}

View file

@ -37,6 +37,13 @@ internal class WcSignUseCaseDelegateTest {
private val rawRequestMock = WcSdkSessionRequest(
topic = "",
chainId = "",
dAppMetaData = WcAppMetaData(
name = "",
description = "",
url = "",
icons = listOf(),
redirect = "",
),
request = WcSdkSessionRequest.JSONRPCRequest(
id = 0L,
method = "",

View file

@ -15,6 +15,11 @@ enum class ValidationResult {
*/
UNSAFE,
/**
* Transaction was confirmed suspicious
*/
WARNING,
/**
* Validation wasn't performed, BlockAid cannot guarantee transaction's safety
*/

View file

@ -19,4 +19,9 @@ sealed class SimulationData {
data class Approve(
val approvedAmounts: List<ApprovedAmount>,
) : SimulationData()
/**
* Simulation was successfully performed and no changes detected
*/
data object NoWalletChangesDetected : SimulationData()
}

View file

@ -173,7 +173,7 @@ internal open class BaseActionsFactory(
}
}
private fun getReceiveScenario(requirements: AssetRequirementsCondition?): ScenarioUnavailabilityReason {
protected fun getReceiveScenario(requirements: AssetRequirementsCondition?): ScenarioUnavailabilityReason {
return when (requirements) {
AssetRequirementsCondition.PaidTransaction,
is AssetRequirementsCondition.PaidTransactionWithFee,

View file

@ -8,6 +8,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState.ActionState
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.walletmanager.WalletManagersFacade
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
@ -65,6 +66,7 @@ internal class CommonActionsFactory(
getSwapUnavailabilityReason(
userWalletId = userWallet.walletId,
currency = cryptoCurrencyStatus.currency,
requirementsDeferred = requirementsDeferred,
)
}
} else {
@ -170,7 +172,16 @@ internal class CommonActionsFactory(
private suspend fun getSwapUnavailabilityReason(
userWalletId: UserWalletId,
currency: CryptoCurrency,
requirementsDeferred: Deferred<AssetRequirementsCondition?>?,
): ScenarioUnavailabilityReason {
return rampStateManager.availableForSwap(userWalletId = userWalletId, cryptoCurrency = currency)
val swapUnavailabilityReason = rampStateManager
.availableForSwap(userWalletId = userWalletId, cryptoCurrency = currency)
val shouldCheckAssetRequirements =
swapUnavailabilityReason == ScenarioUnavailabilityReason.None && requirementsDeferred != null
return if (shouldCheckAssetRequirements) {
getReceiveScenario(requirementsDeferred.await())
} else {
swapUnavailabilityReason
}
}
}

View file

@ -1,8 +1,11 @@
package com.tangem.domain.walletconnect.model.sdkcopy
import kotlinx.serialization.Serializable
/**
* copy of [com.reown.android.Core.Model.AppMetaData]
*/
@Serializable
data class WcAppMetaData(
val name: String,
val description: String,

View file

@ -9,6 +9,7 @@ import kotlinx.serialization.Serializable
data class WcSdkSessionRequest(
val topic: String,
val chainId: String?,
val dAppMetaData: WcAppMetaData,
val request: JSONRPCRequest,
) {

View file

@ -49,8 +49,13 @@ sealed class WcAnalyticEvents(
),
)
data object PairFailed : WcAnalyticEvents(
class PairFailed(
errorCode: String,
) : WcAnalyticEvents(
event = "Session Failed",
params = mapOf(
AnalyticsParam.Key.ERROR_CODE to errorCode,
),
)
class DAppConnected(
@ -83,49 +88,61 @@ sealed class WcAnalyticEvents(
)
class SignatureRequestReceived(
session: WcSession,
rawRequest: WcSdkSessionRequest,
network: Network,
) : WcAnalyticEvents(
event = "Signature Request Received",
params = mapOf(
AnalyticsParam.Key.DAPP_NAME to session.sdkModel.appMetaData.name,
AnalyticsParam.Key.DAPP_URL to session.sdkModel.appMetaData.url,
AnalyticsParam.Key.DAPP_NAME to rawRequest.dAppMetaData.name,
AnalyticsParam.Key.DAPP_URL to rawRequest.dAppMetaData.url,
AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method,
AnalyticsParam.Key.BLOCKCHAIN to network.name,
),
)
class SignatureRequestHandled(
session: WcSession,
rawRequest: WcSdkSessionRequest,
network: Network,
) : WcAnalyticEvents(
event = "Signature Request Handled",
params = mapOf(
AnalyticsParam.Key.DAPP_NAME to session.sdkModel.appMetaData.name,
AnalyticsParam.Key.DAPP_URL to session.sdkModel.appMetaData.url,
AnalyticsParam.Key.DAPP_NAME to rawRequest.dAppMetaData.name,
AnalyticsParam.Key.DAPP_URL to rawRequest.dAppMetaData.url,
AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method,
AnalyticsParam.Key.BLOCKCHAIN to network.name,
),
)
class SignatureRequestFailed(
session: WcSession,
rawRequest: WcSdkSessionRequest,
network: Network,
errorCode: String,
) : WcAnalyticEvents(
event = "Signature Request Failed",
params = mapOf(
AnalyticsParam.Key.DAPP_NAME to session.sdkModel.appMetaData.name,
AnalyticsParam.Key.DAPP_URL to session.sdkModel.appMetaData.url,
AnalyticsParam.Key.DAPP_NAME to rawRequest.dAppMetaData.name,
AnalyticsParam.Key.DAPP_URL to rawRequest.dAppMetaData.url,
AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method,
AnalyticsParam.Key.BLOCKCHAIN to network.name,
AnalyticsParam.Key.ERROR_CODE to errorCode,
),
)
class SignatureRequestReceivedFailed(
rawRequest: WcSdkSessionRequest,
blockchain: String,
errorCode: String,
) : WcAnalyticEvents(
event = "Signature Request Received with Failed",
params = mapOf(
AnalyticsParam.Key.DAPP_NAME to rawRequest.dAppMetaData.name,
AnalyticsParam.Key.DAPP_URL to rawRequest.dAppMetaData.url,
AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method,
AnalyticsParam.Key.BLOCKCHAIN to blockchain,
AnalyticsParam.Key.ERROR_CODE to errorCode,
),
)
class ButtonSign(
rawRequest: WcSdkSessionRequest,
) : WcAnalyticEvents(

View file

@ -34,7 +34,7 @@ interface WcListTransactionUseCase :
* [updateFee] triggered a new [TransactionData] emit
*/
interface WcMutableFee {
fun dAppFee(): Fee?
suspend fun dAppFee(): Fee?
fun updateFee(fee: Fee)
}

View file

@ -85,7 +85,13 @@ internal class OnrampRedirectModel @Inject constructor(
.onLeft(::handleError)
.onRight {
latestOnrampTransaction = it
urlOpener.openUrl(it.redirectUrl)
// Workaround to open Unlimit provider in external browser instead of chrome custom tabs
if (params.onrampProviderWithQuote.provider.id.equals(UNLIMIT_PROVIDER_ID, ignoreCase = true)) {
urlOpener.openUrlExternalBrowser(it.redirectUrl)
} else {
urlOpener.openUrl(it.redirectUrl)
}
}
}
}
@ -126,4 +132,8 @@ internal class OnrampRedirectModel @Inject constructor(
messageSender.send(message)
}
private companion object {
const val UNLIMIT_PROVIDER_ID = "unlimit"
}
}

View file

@ -12,6 +12,7 @@ internal class AlertsComponentV2(
) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent {
override fun dismiss() {
messageUM.onDismissRequest()
router.pop()
}

View file

@ -115,8 +115,11 @@ internal class WcPairComponent(
is Alert.Type.Verified -> WcAlertsFactory.createVerifiedDomainAlert(alertType.appName)
is Alert.Type.UnknownDomain -> WcAlertsFactory.createUnknownDomainAlert(model::connectFromAlert)
is Alert.Type.UnsafeDomain -> WcAlertsFactory.createUnsafeDomainAlert(model::connectFromAlert)
is Alert.Type.UnsupportedDApp -> WcAlertsFactory.createUnsupportedDomainAlert(alertType.appName)
is Alert.Type.UnsupportedNetwork -> WcAlertsFactory.createUnsupportedChainAlert(alertType.appName)
is Alert.Type.UnsupportedDApp ->
WcAlertsFactory.createUnsupportedDomainAlert(alertType.appName, model::errorAlertOnDismiss)
is Alert.Type.UnsupportedNetwork ->
WcAlertsFactory.createUnsupportedChainAlert(alertType.appName, model::errorAlertOnDismiss)
is Alert.Type.UriAlreadyUsed -> WcAlertsFactory.createUriAlreadyUsedAlert(model::errorAlertOnDismiss)
}
}

View file

@ -149,6 +149,10 @@ internal class WcPairModel @Inject constructor(
connect()
}
fun errorAlertOnDismiss() {
router.pop()
}
private fun onConnect(securityStatus: CheckDAppResult) {
when (securityStatus) {
CheckDAppResult.SAFE -> connect()
@ -193,16 +197,23 @@ internal class WcPairModel @Inject constructor(
}
private fun processError(error: WcPairError) {
when (error) {
val alert = when (error) {
is WcPairError.UnsupportedDApp -> {
WcAppInfoRoutes.Alert.Type.UnsupportedDApp(error.appName)
}
is WcPairError.UnsupportedBlockchains -> {
WcAppInfoRoutes.Alert.Type.UnsupportedNetwork(error.appName)
}
else -> null
}?.let { stackNavigation.pushNew(WcAppInfoRoutes.Alert(it)) }
?: run { messageSender.send(ToastMessage(message = stringReference(error.message))) }
is WcPairError.UriAlreadyUsed -> {
WcAppInfoRoutes.Alert.Type.UriAlreadyUsed
}
else -> {
messageSender.send(ToastMessage(message = stringReference(error.message)))
router.pop()
null
}
}
alert?.let { stackNavigation.pushNew(WcAppInfoRoutes.Alert(it)) }
}
override fun onWalletSelected(userWalletId: UserWalletId) {

View file

@ -34,6 +34,7 @@ internal sealed class WcAppInfoRoutes : TangemBottomSheetConfigContent, Route {
data object UnsafeDomain : Type()
data class UnsupportedDApp(val appName: String) : Type()
data class UnsupportedNetwork(val appName: String) : Type()
data object UriAlreadyUsed : Type()
}
}
}

View file

@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -311,23 +312,28 @@ private fun WalletRowItem(walletName: String, showEndIcon: Boolean, modifier: Mo
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
)
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing4)
.weight(1f),
text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing16)
.weight(1f),
text = walletName,
textAlign = TextAlign.End,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
)
Row(
modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
)
Text(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
text = walletName,
textAlign = TextAlign.End,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if (showEndIcon) {
Icon(
modifier = Modifier
@ -611,7 +617,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider<WcAppI
verifiedDAppState = VerifiedDAppState.Verified {},
appSubtitle = "react-app.walletconnect.com",
notification = WcAppInfoSecurityNotification.SecurityRisk,
walletName = "Tangem 2.0",
walletName = "Tangem 2.0 Tangem 2.0 Tangem 2.0 Tangem 2.0",
onWalletClick = null,
networksInfo = WcNetworksInfo.ContainsAllRequiredNetworks(
items = persistentListOf(
@ -657,7 +663,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider<WcAppI
verifiedDAppState = VerifiedDAppState.Unknown,
appSubtitle = "react-app.walletconnect.com",
notification = WcAppInfoSecurityNotification.UnknownDomain,
walletName = "Tangem 2.0",
walletName = "Tangem 2.0 Tangem 2.0 Tangem 2.0 Tangem 2.0",
onWalletClick = {},
networksInfo = WcNetworksInfo.MissingRequiredNetworkInfo(networks = "Solana"),
onNetworksClick = {},

View file

@ -17,10 +17,10 @@ internal object WcAlertsFactory {
createUnknownDomainAlert()
is WcTransactionRoutes.Alert.Type.UnsafeDomain ->
createUnsafeDomainAlert()
is WcTransactionRoutes.Alert.Type.MaliciousInfo ->
createMaliciousDAppAlert(alertType.description, alertType.onClick)
is WcTransactionRoutes.Alert.Type.BlockAidErrorInfo ->
createMaliciousDAppAlert(alertType.description, alertType.onClick, alertType.iconType, alertType.iconBgType)
is WcTransactionRoutes.Alert.Type.UnknownError ->
createUnknownErrorAlert(alertType.errorMessage, alertType.onDismiss)
createUnknownErrorAlert(alertType.errorMessage, alertType.onDismiss, alertType.onRetry)
}
fun createUnknownDomainAlert(activeButtonOnClick: (() -> Unit)? = null): MessageBottomSheetUMV2 {
@ -74,11 +74,11 @@ internal object WcAlertsFactory {
}
}
fun createUnsupportedDomainAlert(appName: String): MessageBottomSheetUMV2 {
fun createUnsupportedDomainAlert(appName: String, onDismiss: () -> Unit): MessageBottomSheetUMV2 {
return messageBottomSheetUM {
infoBlock {
icon(R.drawable.ic_wallet_connect_24) {
type = MessageBottomSheetUMV2.Icon.Type.Informative
type = Type.Informative
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = resourceReference(R.string.wc_alert_unsupported_dapps_title)
@ -86,16 +86,34 @@ internal object WcAlertsFactory {
}
primaryButton {
text = resourceReference(R.string.common_got_it)
onClick { closeBs() }
onClick { onDismiss() }
}
onDismissRequest = onDismiss
}
}
fun createUriAlreadyUsedAlert(onDismiss: () -> Unit): MessageBottomSheetUMV2 {
return messageBottomSheetUM {
infoBlock {
icon(R.drawable.ic_wallet_connect_24) {
type = Type.Informative
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = resourceReference(R.string.wc_uri_already_used_title)
body = resourceReference(R.string.wc_uri_already_used_description)
}
primaryButton {
text = resourceReference(R.string.common_got_it)
onClick { onDismiss() }
}
}
}
fun createUnsupportedChainAlert(appName: String): MessageBottomSheetUMV2 {
fun createUnsupportedChainAlert(appName: String, onDismiss: () -> Unit): MessageBottomSheetUMV2 {
return messageBottomSheetUM {
infoBlock {
icon(R.drawable.ic_network_new_24) {
type = MessageBottomSheetUMV2.Icon.Type.Informative
type = Type.Informative
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = resourceReference(R.string.wc_alert_unsupported_networks_title)
@ -103,12 +121,17 @@ internal object WcAlertsFactory {
}
primaryButton {
text = resourceReference(R.string.common_got_it)
onClick { closeBs() }
onClick { onDismiss() }
}
onDismissRequest = onDismiss
}
}
private fun createUnknownErrorAlert(errorMessage: String?, onDismiss: () -> Unit): MessageBottomSheetUMV2 {
private fun createUnknownErrorAlert(
errorMessage: String?,
onDismiss: () -> Unit,
onRetry: () -> Unit,
): MessageBottomSheetUMV2 {
return messageBottomSheetUM {
infoBlock {
icon(R.drawable.img_attention_20) {
@ -124,21 +147,29 @@ internal object WcAlertsFactory {
)
}
}
primaryButton {
text = resourceReference(R.string.alert_button_try_again)
onClick { onRetry() }
}
secondaryButton {
text = resourceReference(R.string.balance_hidden_got_it_button)
text = resourceReference(R.string.common_cancel)
onClick { onDismiss() }
}
onDismissRequest = onDismiss
}
}
private fun createMaliciousDAppAlert(
description: String?,
activeButtonOnClick: (() -> Unit),
iconType: Type,
iconBgType: MessageBottomSheetUMV2.Icon.BackgroundType,
): MessageBottomSheetUMV2 {
return messageBottomSheetUM {
infoBlock {
icon(R.drawable.img_knight_shield_32) {
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Attention
type = iconType
backgroundType = iconBgType
}
title = resourceReference(R.string.security_alert_title)
if (!description.isNullOrEmpty()) {

View file

@ -7,6 +7,7 @@ import com.arkivanov.essenty.lifecycle.doOnResume
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionFeeState
import com.tangem.features.walletconnect.transaction.model.WcSendTransactionModel
@ -58,6 +59,7 @@ internal class WcSendTransactionComponent(
WcSendTransactionModalBottomSheet(
state = state,
feeSelectorBlockComponent = feeSelectorBlock,
feeSelectorUM = content?.feeSelectorUM ?: FeeSelectorUM.Loading,
onClickTransactionRequest = model::showTransactionRequest,
onBack = router::pop,
onDismiss = ::dismiss,

View file

@ -1,24 +1,14 @@
package com.tangem.features.walletconnect.transaction.converter
import com.tangem.domain.walletconnect.usecase.method.WcNetworkDerivationState
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
import com.tangem.utils.converter.Converter
private const val ADDRESS_FIRST_PART_LENGTH = 7
private const val ADDRESS_SECOND_PART_LENGTH = 4
internal object WcAddressConverter : Converter<WcNetworkDerivationState, String?> {
internal object WcAddressConverter : Converter<WcNetworkDerivationState, WcAddressUM?> {
override fun convert(value: WcNetworkDerivationState): WcAddressUM? {
override fun convert(value: WcNetworkDerivationState): String? {
return when (value) {
is WcNetworkDerivationState.Single -> null
is WcNetworkDerivationState.Multiple -> WcAddressUM(
fullAddress = value.walletAddress,
shortAddress = value.walletAddress.toShortAddressText(),
)
is WcNetworkDerivationState.Multiple -> value.walletAddress
}
}
private fun String.toShortAddressText() =
"${take(ADDRESS_FIRST_PART_LENGTH)}...${takeLast(ADDRESS_SECOND_PART_LENGTH)}"
}

View file

@ -1,62 +0,0 @@
package com.tangem.features.walletconnect.transaction.converter
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcSignUseCase
import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM
import com.tangem.utils.converter.Converter
import javax.inject.Inject
internal class WcCommonTransactionUMConverter @Inject constructor(
private val signTypedDataUMConverter: WcSignTypedDataUMConverter,
private val signTransactionUMConverter: WcSignTransactionUMConverter,
private val sendTransactionUMConverter: WcSendTransactionUMConverter,
) : Converter<WcCommonTransactionUMConverter.Input, WcCommonTransactionUM?> {
override fun convert(value: Input): WcCommonTransactionUM? {
return when (value.useCase) {
is WcMessageSignUseCase -> {
when (value.useCase.method) {
is WcEthMethod.SignTypedData -> signTypedDataUMConverter.convert(
WcSignTypedDataUMConverter.Input(
useCase = value.useCase,
signState = value.signState,
signModel = value.signState.signModel as WcMessageSignUseCase.SignModel,
actions = value.actions,
),
)
is WcEthMethod.MessageSign, is WcSolanaMethod.SignMessage -> signTransactionUMConverter.convert(
WcSignTransactionUMConverter.Input(
useCase = value.useCase,
signState = value.signState,
signModel = value.signState.signModel as WcMessageSignUseCase.SignModel,
actions = value.actions,
),
)
else -> null
}
}
is WcTransactionUseCase -> sendTransactionUMConverter.convert(
WcSendTransactionUMConverter.Input(
useCase = value.useCase,
signState = value.signState,
actions = value.actions,
feeSelectorUM = value.feeSelectorUM,
),
)
else -> null
}
}
data class Input(
val useCase: WcSignUseCase<*>,
val signState: WcSignState<*>,
val actions: WcTransactionActionsUM,
val feeSelectorUM: FeeSelectorUM? = null,
)
}

View file

@ -1,8 +1,11 @@
package com.tangem.features.walletconnect.transaction.converter
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.*
import com.tangem.domain.walletconnect.usecase.method.WcMethodContext
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM
@ -10,6 +13,7 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM
import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionItemUM
import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionUM
import com.tangem.features.walletconnect.utils.WcNotificationsFactory
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject
@ -18,58 +22,62 @@ internal class WcSendTransactionUMConverter @Inject constructor(
private val appInfoContentUMConverter: WcTransactionAppInfoContentUMConverter,
private val networkInfoUMConverter: WcNetworkInfoUMConverter,
private val requestBlockUMConverter: WcTransactionRequestBlockUMConverter,
private val notificationsFactory: WcNotificationsFactory,
) : Converter<WcSendTransactionUMConverter.Input, WcSendTransactionUM?> {
override fun convert(value: Input): WcSendTransactionUM? = when (value.useCase.method) {
is WcEthMethod.SendTransaction,
is WcEthMethod.SignTransaction,
is WcSolanaMethod.SignAllTransaction,
is WcSolanaMethod.SignTransaction,
-> WcSendTransactionUM(
transaction = WcSendTransactionItemUM(
onDismiss = value.actions.onDismiss,
onSend = value.actions.onSign,
appInfo = appInfoContentUMConverter.convert(
WcTransactionAppInfoContentUMConverter.Input(
session = value.useCase.session,
onShowVerifiedAlert = value.actions.onShowVerifiedAlert,
),
),
feeState = constructFeeState(useCase = value.useCase, actions = value.actions),
walletName = value.useCase.session.wallet.name.takeIf { value.useCase.session.showWalletInfo },
networkInfo = networkInfoUMConverter.convert(value.useCase.network),
estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM(),
isLoading = value.signState.domainStep == WcSignStep.Signing,
address = WcAddressConverter.convert(value.useCase.derivationState),
),
feeSelectorUM = value.feeSelectorUM ?: FeeSelectorUM.Loading,
transactionRequestInfo = WcTransactionRequestInfoUM(
blocks = buildList {
addAll(
requestBlockUMConverter.convert(
WcTransactionRequestBlockUMConverter.Input(value.useCase.rawSdkRequest),
),
)
}.toImmutableList(),
onCopy = value.actions.onCopy,
),
override fun convert(value: Input): WcSendTransactionUM? {
val feeErrorNotification = notificationsFactory.createFeeNotifications(
cryptoCurrencyStatus = value.cryptoCurrencyStatus,
feeSelectorUM = value.feeSelectorUM,
onFeeReload = value.onFeeReload,
)
else -> null
}
private fun constructFeeState(
useCase: WcTransactionUseCase,
actions: WcTransactionActionsUM,
): WcTransactionFeeState {
val mutableFee = useCase as? WcMutableFee ?: return WcTransactionFeeState.None
val dAppFee = mutableFee.dAppFee()
return WcTransactionFeeState.Success(dAppFee = dAppFee, onClick = actions.onShowFeeBottomSheet)
return when (value.context.method) {
is WcEthMethod.SendTransaction,
is WcEthMethod.SignTransaction,
is WcSolanaMethod.SignAllTransaction,
is WcSolanaMethod.SignTransaction,
-> WcSendTransactionUM(
transaction = WcSendTransactionItemUM(
onDismiss = value.actions.onDismiss,
onSend = value.actions.onSign,
appInfo = appInfoContentUMConverter.convert(
WcTransactionAppInfoContentUMConverter.Input(
session = value.context.session,
onShowVerifiedAlert = value.actions.onShowVerifiedAlert,
),
),
feeState = value.feeState,
walletName = value.context.session.wallet.name.takeIf { value.context.session.showWalletInfo },
networkInfo = networkInfoUMConverter.convert(value.context.network),
estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM(),
isLoading = value.signState.domainStep == WcSignStep.Signing,
address = WcAddressConverter.convert(value.context.derivationState),
sendEnabled = value.feeSelectorUM is FeeSelectorUM.Content && feeErrorNotification == null,
feeErrorNotification = feeErrorNotification,
),
feeSelectorUM = value.feeSelectorUM ?: FeeSelectorUM.Loading,
transactionRequestInfo = WcTransactionRequestInfoUM(
blocks = buildList {
addAll(
requestBlockUMConverter.convert(
WcTransactionRequestBlockUMConverter.Input(value.context.rawSdkRequest),
),
)
}.toImmutableList(),
onCopy = value.actions.onCopy,
),
)
else -> null
}
}
data class Input(
val useCase: WcTransactionUseCase,
val context: WcMethodContext,
val feeState: WcTransactionFeeState,
val signState: WcSignState<*>,
val actions: WcTransactionActionsUM,
val feeSelectorUM: FeeSelectorUM?,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val onFeeReload: () -> Unit,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.walletconnect.transaction.converter
import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase
import com.tangem.domain.walletconnect.usecase.method.WcMethodContext
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM
@ -23,25 +24,25 @@ internal class WcSignTransactionUMConverter @Inject constructor(
onSign = value.actions.onSign,
appInfo = appInfoContentUMConverter.convert(
WcTransactionAppInfoContentUMConverter.Input(
session = value.useCase.session,
session = value.context.session,
onShowVerifiedAlert = value.actions.onShowVerifiedAlert,
),
),
walletName = value.useCase.session.wallet.name.takeIf { value.useCase.session.showWalletInfo },
networkInfo = networkInfoUMConverter.convert(value.useCase.network),
walletName = value.context.session.wallet.name.takeIf { value.context.session.showWalletInfo },
networkInfo = networkInfoUMConverter.convert(value.context.network),
isLoading = value.signState.domainStep == WcSignStep.Signing,
address = WcAddressConverter.convert(value.useCase.derivationState),
address = WcAddressConverter.convert(value.context.derivationState),
),
transactionRequestInfo = WcTransactionRequestInfoUM(
requestBlockUMConverter.convert(
WcTransactionRequestBlockUMConverter.Input(value.useCase.rawSdkRequest, value.signModel),
WcTransactionRequestBlockUMConverter.Input(value.context.rawSdkRequest, value.signModel),
).toImmutableList(),
onCopy = value.actions.onCopy,
),
)
data class Input(
val useCase: WcMessageSignUseCase,
val context: WcMethodContext,
val signState: WcSignState<*>,
val signModel: WcMessageSignUseCase.SignModel,
val actions: WcTransactionActionsUM,

View file

@ -1,6 +1,7 @@
package com.tangem.features.walletconnect.transaction.converter
import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase
import com.tangem.domain.walletconnect.usecase.method.WcMethodContext
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM
@ -23,20 +24,20 @@ internal class WcSignTypedDataUMConverter @Inject constructor(
onSign = value.actions.onSign,
appInfo = appInfoContentUMConverter.convert(
WcTransactionAppInfoContentUMConverter.Input(
session = value.useCase.session,
session = value.context.session,
onShowVerifiedAlert = value.actions.onShowVerifiedAlert,
),
),
walletName = value.useCase.session.wallet.name.takeIf { value.useCase.session.showWalletInfo },
networkInfo = networkInfoUMConverter.convert(value.useCase.network),
address = WcAddressConverter.convert(value.useCase.derivationState),
walletName = value.context.session.wallet.name.takeIf { value.context.session.showWalletInfo },
networkInfo = networkInfoUMConverter.convert(value.context.network),
address = WcAddressConverter.convert(value.context.derivationState),
isLoading = value.signState.domainStep == WcSignStep.Signing,
),
transactionRequestInfo = WcTransactionRequestInfoUM(
blocks = buildList {
addAll(
requestBlockUMConverter.convert(
WcTransactionRequestBlockUMConverter.Input(value.useCase.rawSdkRequest, value.signModel),
WcTransactionRequestBlockUMConverter.Input(value.context.rawSdkRequest, value.signModel),
),
)
}.toImmutableList(),
@ -45,7 +46,7 @@ internal class WcSignTypedDataUMConverter @Inject constructor(
)
data class Input(
val useCase: WcMessageSignUseCase,
val context: WcMethodContext,
val signState: WcSignState<*>,
val signModel: WcMessageSignUseCase.SignModel,
val actions: WcTransactionActionsUM,

View file

@ -6,6 +6,17 @@ import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllow
internal data class WcSendReceiveTransactionCheckResultsUM(
val estimatedWalletChanges: WcEstimatedWalletChangesUM? = null,
val spendAllowance: WcSpendAllowanceUM? = null,
val notificationText: TextReference? = null,
val notification: BlockAidNotificationUM? = null,
val additionalNotification: TextReference? = null,
val isLoading: Boolean = true,
)
)
internal data class BlockAidNotificationUM(
val type: Type,
val title: TextReference,
val text: TextReference? = null,
) {
internal enum class Type {
ERROR, WARNING
}
}

View file

@ -1,3 +0,0 @@
package com.tangem.features.walletconnect.transaction.entity.common
data class WcAddressUM(val fullAddress: String, val shortAddress: String)

View file

@ -1,10 +1,10 @@
package com.tangem.features.walletconnect.transaction.entity.send
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionUM
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM
@ -26,6 +26,8 @@ internal data class WcSendTransactionItemUM(
val estimatedWalletChanges: WcSendReceiveTransactionCheckResultsUM?,
val walletName: String?,
val networkInfo: WcNetworkInfoUM,
val address: WcAddressUM?,
val address: String?,
val sendEnabled: Boolean,
val feeErrorNotification: NotificationUM.Info?,
val isLoading: Boolean = false,
) : TangemBottomSheetConfigContent

View file

@ -1,7 +1,6 @@
package com.tangem.features.walletconnect.transaction.entity.sign
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionUM
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM
@ -18,6 +17,6 @@ internal data class WcSignTransactionItemUM(
val appInfo: WcTransactionAppInfoContentUM,
val walletName: String?,
val networkInfo: WcNetworkInfoUM,
val address: WcAddressUM?,
val address: String?,
val isLoading: Boolean = false,
) : TangemBottomSheetConfigContent

View file

@ -15,6 +15,8 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2.Icon.Type
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -36,14 +38,16 @@ import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorRelo
import com.tangem.features.send.v2.api.subcomponents.feeSelector.entity.FeeSelectorData
import com.tangem.features.walletconnect.connections.routing.WcInnerRoute
import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams
import com.tangem.features.walletconnect.transaction.converter.WcCommonTransactionUMConverter
import com.tangem.features.walletconnect.transaction.converter.WcHandleMethodErrorConverter
import com.tangem.features.walletconnect.transaction.converter.WcSendTransactionUMConverter
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM
import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionModel
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionFeeState
import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionUM
import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes
import com.tangem.features.walletconnect.transaction.ui.blockaid.WcSendAndReceiveBlockAidUiConverter
import com.tangem.features.walletconnect.utils.WcNotificationsFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -62,10 +66,11 @@ internal class WcSendTransactionModel @Inject constructor(
private val router: Router,
private val clipboardManager: ClipboardManager,
private val useCaseFactory: WcRequestUseCaseFactory,
private val converter: WcCommonTransactionUMConverter,
private val converter: WcSendTransactionUMConverter,
private val blockAidUiConverter: WcSendAndReceiveBlockAidUiConverter,
private val getFeeUseCase: GetFeeUseCase,
private val getNetworkCoinUseCase: GetNetworkCoinStatusUseCase,
private val notificationsFactory: WcNotificationsFactory,
private val analytics: AnalyticsEventHandler,
) : Model(), WcCommonTransactionModel, FeeSelectorModelCallback {
@ -137,7 +142,7 @@ internal class WcSendTransactionModel @Inject constructor(
feeReloadState.value = false
modelScope.launch {
feeSelectorReloadTrigger.triggerUpdate(
feeData = FeeSelectorData(removeSuggestedFee = true),
FeeSelectorData(removeSuggestedFee = feeStateConfiguration !is FeeStateConfiguration.Suggestion),
)
}
}
@ -156,7 +161,20 @@ internal class WcSendTransactionModel @Inject constructor(
* Also handles fee results from FeeSelectorBlockComponent
*/
fun updateFee(feeSelectorUM: FeeSelectorUM) {
_uiState.update { it?.copy(feeSelectorUM = feeSelectorUM) }
val feeErrorNotification = notificationsFactory.createFeeNotifications(
cryptoCurrencyStatus = cryptoCurrencyStatus,
feeSelectorUM = feeSelectorUM,
onFeeReload = ::triggerFeeReload,
)
_uiState.update {
it?.copy(
feeSelectorUM = feeSelectorUM,
transaction = it.transaction.copy(
sendEnabled = feeSelectorUM is FeeSelectorUM.Content && feeErrorNotification == null,
feeErrorNotification = feeErrorNotification,
),
)
}
val fee = (feeSelectorUM as? FeeSelectorUM.Content)?.selectedFeeItem?.fee ?: return
(useCase as? WcMutableFee)?.updateFee(fee)
}
@ -198,20 +216,31 @@ internal class WcSendTransactionModel @Inject constructor(
is Lce.Error -> WcSendReceiveTransactionCheckResultsUM(isLoading = false)
is Lce.Loading -> WcSendReceiveTransactionCheckResultsUM(isLoading = true)
}
val feeState = when {
useCase is WcMutableFee -> WcTransactionFeeState.Success(
dAppFee = useCase.dAppFee(),
onClick = ::onShowFeeBottomSheet,
)
else -> WcTransactionFeeState.None
}
val actions = WcTransactionActionsUM(
onShowVerifiedAlert = ::showVerifiedAlert,
onDismiss = { cancel(useCase) },
onSign = { onSign(securityCheck.getOrNull()) },
onCopy = { copyData(useCase.rawSdkRequest.request.params) },
)
var transactionUM = converter.convert(
WcCommonTransactionUMConverter.Input(
useCase = useCase,
WcSendTransactionUMConverter.Input(
context = useCase,
feeState = feeState,
signState = signState,
actions = WcTransactionActionsUM(
onShowVerifiedAlert = ::showVerifiedAlert,
onDismiss = { cancel(useCase) },
onSign = { onSign(securityCheck.getOrNull()) },
onCopy = { copyData(useCase.rawSdkRequest.request.params) },
onShowFeeBottomSheet = ::onShowFeeBottomSheet,
),
actions = actions,
feeSelectorUM = uiState.value?.feeSelectorUM,
cryptoCurrencyStatus = cryptoCurrencyStatus,
onFeeReload = ::triggerFeeReload,
),
) as? WcSendTransactionUM
)
transactionUM = transactionUM?.copy(
transaction = transactionUM.transaction.copy(estimatedWalletChanges = blockAidState),
spendAllowance = blockAidState.spendAllowance,
@ -236,10 +265,10 @@ internal class WcSendTransactionModel @Inject constructor(
}
private fun onSign(securityCheck: BlockAidTransactionCheck.Result?) {
if (securityCheck?.result?.validation == ValidationResult.UNSAFE) {
showMaliciousAlert(securityCheck.result.description)
} else {
sign()
when (securityCheck?.result?.validation) {
ValidationResult.UNSAFE -> showMaliciousAlert(securityCheck.result.description)
ValidationResult.WARNING -> showWarningAlert(securityCheck.result.description)
else -> sign()
}
securityCheck?.result?.validation?.let { securityStatus ->
val event = WcAnalyticEvents.NoticeSecurityAlert(
@ -250,6 +279,7 @@ internal class WcSendTransactionModel @Inject constructor(
when (securityStatus) {
ValidationResult.SAFE -> Unit
ValidationResult.UNSAFE,
ValidationResult.WARNING,
ValidationResult.FAILED_TO_VALIDATE,
-> analytics.send(event)
}
@ -273,7 +303,22 @@ internal class WcSendTransactionModel @Inject constructor(
}
private fun showMaliciousAlert(description: String?) {
val type = WcTransactionRoutes.Alert.Type.MaliciousInfo(description = description, onClick = ::signFromAlert)
val type = WcTransactionRoutes.Alert.Type.BlockAidErrorInfo(
description = description,
onClick = ::signFromAlert,
iconType = Type.Warning,
iconBgType = MessageBottomSheetUMV2.Icon.BackgroundType.Warning,
)
stackNavigation.pushNew(WcTransactionRoutes.Alert(type))
}
private fun showWarningAlert(description: String?) {
val type = WcTransactionRoutes.Alert.Type.BlockAidErrorInfo(
description = description,
onClick = ::signFromAlert,
iconType = Type.Attention,
iconBgType = MessageBottomSheetUMV2.Icon.BackgroundType.Attention,
)
stackNavigation.pushNew(WcTransactionRoutes.Alert(type))
}
@ -284,22 +329,24 @@ internal class WcSendTransactionModel @Inject constructor(
private fun signingIsDone(signState: WcSignState<*>, useCase: WcSignUseCase<*>): Boolean {
(signState.domainStep as? WcSignStep.Result)?.result?.let {
handleSigningError(it, useCase)
return true
return handleSigningError(it, useCase)
}
return false
}
private fun handleSigningError(result: Either<WcRequestError, String>, useCase: WcSignUseCase<*>) {
if (result.isLeft()) {
private fun handleSigningError(result: Either<WcRequestError, String>, useCase: WcSignUseCase<*>): Boolean {
return if (result.isLeft()) {
val error = WcTransactionRoutes.Alert.Type.UnknownError(
errorMessage = result.leftOrNull()?.message(),
onDismiss = { cancel(useCase) },
onRetry = { signFromAlert() },
)
stackNavigation.pushNew(WcTransactionRoutes.Alert(error))
false
} else {
showSuccessSignMessage()
router.pop()
true
}
}

View file

@ -11,13 +11,16 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
import com.tangem.domain.walletconnect.usecase.method.WcSignUseCase
import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams
import com.tangem.features.walletconnect.transaction.converter.WcCommonTransactionUMConverter
import com.tangem.features.walletconnect.transaction.converter.WcHandleMethodErrorConverter
import com.tangem.features.walletconnect.transaction.converter.WcSignTransactionUMConverter
import com.tangem.features.walletconnect.transaction.converter.WcSignTypedDataUMConverter
import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionModel
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionUM
@ -40,7 +43,8 @@ internal class WcSignTransactionModel @Inject constructor(
private val router: Router,
private val clipboardManager: ClipboardManager,
private val useCaseFactory: WcRequestUseCaseFactory,
private val converter: WcCommonTransactionUMConverter,
private val signTypedDataUMConverter: WcSignTypedDataUMConverter,
private val signTransactionUMConverter: WcSignTransactionUMConverter,
) : Model(), WcCommonTransactionModel {
private val params = paramsContainer.require<WcTransactionModelParams>()
@ -58,24 +62,44 @@ internal class WcSignTransactionModel @Inject constructor(
useCase.invoke()
.onEach { signState ->
if (signingIsDone(signState)) return@onEach
val signTransactionUM = converter.convert(
WcCommonTransactionUMConverter.Input(
useCase = useCase,
signState = signState,
actions = WcTransactionActionsUM(
onShowVerifiedAlert = ::showVerifiedAlert,
onDismiss = { cancel(useCase) },
onSign = useCase::sign,
onCopy = { copyData(useCase.rawSdkRequest.request.params) },
),
),
) as? WcSignTransactionUM
val signTransactionUM = convertToUI(useCase, signState)
_uiState.emit(signTransactionUM)
}
.launchIn(this)
}
}
private fun convertToUI(
useCase: WcMessageSignUseCase,
signState: WcSignState<WcMessageSignUseCase.SignModel>,
): WcSignTransactionUM? {
val actions = WcTransactionActionsUM(
onShowVerifiedAlert = ::showVerifiedAlert,
onDismiss = { cancel(useCase) },
onSign = useCase::sign,
onCopy = { copyData(useCase.rawSdkRequest.request.params) },
)
return when (useCase.method) {
is WcEthMethod.SignTypedData -> signTypedDataUMConverter.convert(
WcSignTypedDataUMConverter.Input(
context = useCase,
signState = signState,
signModel = signState.signModel,
actions = actions,
),
)
is WcEthMethod.MessageSign, is WcSolanaMethod.SignMessage -> signTransactionUMConverter.convert(
WcSignTransactionUMConverter.Input(
context = useCase,
signState = signState,
signModel = signState.signModel,
actions = actions,
),
)
else -> null
}
}
override fun dismiss() {
_uiState.value?.transaction?.onDismiss?.invoke() ?: router.pop()
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.walletconnect.transaction.routes
import androidx.compose.runtime.Immutable
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import kotlinx.serialization.Serializable
@Serializable
@ -27,8 +28,17 @@ internal sealed class WcTransactionRoutes : TangemBottomSheetConfigContent, Rout
data class Verified(val appName: String) : Type()
data object UnknownDomain : Type()
data object UnsafeDomain : Type()
data class MaliciousInfo(val description: String?, val onClick: () -> Unit) : Type()
data class UnknownError(val errorMessage: String?, val onDismiss: () -> Unit) : Type()
data class BlockAidErrorInfo(
val description: String?,
val onClick: () -> Unit,
val iconType: MessageBottomSheetUMV2.Icon.Type,
val iconBgType: MessageBottomSheetUMV2.Icon.BackgroundType,
) : Type()
data class UnknownError(
val errorMessage: String?,
val onDismiss: () -> Unit,
val onRetry: () -> Unit,
) : Type()
}
}
}

View file

@ -11,11 +11,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.isNullOrEmpty
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.entity.blockaid.BlockAidNotificationUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM
@ -36,15 +37,17 @@ internal fun TransactionCheckResultsItem(
if (item.isLoading) {
WcEstimatedWalletChangesLoadingItem()
} else {
if (item.notificationText != null) {
WcTransactionCheckErrorItem(item.notificationText.resolveReference())
if (item.notification != null) {
WcTransactionCheckErrorItem(item.notification)
}
if (item.estimatedWalletChanges != null) {
WcEstimatedWalletChangesItem(item.estimatedWalletChanges)
} else if (item.spendAllowance != null) {
WcSpendAllowanceItem(item.spendAllowance, onClickAllowToSpend)
} else if (!item.additionalNotification.isNullOrEmpty()) {
WcEstimatedWalletChangesNotificationItem(description = item.additionalNotification)
} else {
WcEstimatedWalletChangesNotLoadedItem()
WcEstimatedWalletChangesNotificationItem()
}
}
}
@ -70,7 +73,11 @@ private class TransactionCheckResultsItemProvider : PreviewParameterProvider<WcS
override val values = sequenceOf(
WcSendReceiveTransactionCheckResultsUM(
isLoading = false,
notificationText = TextReference.Str("The transaction approves erc20 tokens to a known malicious address"),
notification = BlockAidNotificationUM(
type = BlockAidNotificationUM.Type.ERROR,
title = TextReference.Res(R.string.wc_malicious_transaction),
text = TextReference.Str("The transaction approves erc20 tokens to a known malicious address"),
),
estimatedWalletChanges = WcEstimatedWalletChangesUM(
items = persistentListOf(
WcEstimatedWalletChangeUM(

View file

@ -12,6 +12,8 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -19,7 +21,10 @@ import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem
@Composable
internal fun WcEstimatedWalletChangesNotLoadedItem(modifier: Modifier = Modifier) {
internal fun WcEstimatedWalletChangesNotificationItem(
modifier: Modifier = Modifier,
description: TextReference? = null,
) {
Column(
modifier = modifier
.clip(RoundedCornerShape(14.dp))
@ -36,7 +41,8 @@ internal fun WcEstimatedWalletChangesNotLoadedItem(modifier: Modifier = Modifier
modifier = modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, bottom = 14.dp),
text = stringResourceSafe(R.string.wc_estimated_wallet_changes_not_simulated),
text = description?.resolveReference()
?: stringResourceSafe(R.string.wc_estimated_wallet_changes_not_simulated),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
)
@ -52,7 +58,7 @@ private fun WcEstimatedWalletChangesNotLoadedItems() {
modifier = Modifier
.background(TangemTheme.colors.background.tertiary),
) {
WcEstimatedWalletChangesNotLoadedItem()
WcEstimatedWalletChangesNotificationItem()
}
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.walletconnect.model.WcApprovedAmount
import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.entity.blockaid.BlockAidNotificationUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM
@ -22,22 +23,39 @@ import javax.inject.Inject
internal const val DECIMALS_AMOUNT = 2
@Suppress("CyclomaticComplexMethod")
@Suppress("CyclomaticComplexMethod", "LongMethod")
internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor(
private val estimatedWalletChangeUMConverter: WcEstimatedWalletChangeUMConverter,
private val spendAllowanceUMConverter: WcSpendAllowanceUMConverter,
) : Converter<WcSendAndReceiveBlockAidUiConverter.Input, WcSendReceiveTransactionCheckResultsUM> {
override fun convert(value: Input): WcSendReceiveTransactionCheckResultsUM {
val description = value.result.description
val description = value.result.description?.let { if (it.isNotEmpty()) TextReference.Str(it) else null }
val simulation = value.result.simulation
return WcSendReceiveTransactionCheckResultsUM(
isLoading = false,
notificationText = when (value.result.validation) {
notification = when (value.result.validation) {
ValidationResult.SAFE, ValidationResult.FAILED_TO_VALIDATE -> null
ValidationResult.UNSAFE -> if (!description.isNullOrEmpty()) TextReference.Str(description) else null
ValidationResult.UNSAFE -> BlockAidNotificationUM(
type = BlockAidNotificationUM.Type.ERROR,
title = TextReference.Res(R.string.wc_malicious_transaction),
text = description,
)
ValidationResult.WARNING -> BlockAidNotificationUM(
type = BlockAidNotificationUM.Type.WARNING,
title = TextReference.Res(R.string.wc_warning_transaction),
text = description,
)
},
estimatedWalletChanges = (value.result.simulation as? SimulationResult.Success)?.data?.let { data ->
additionalNotification = (simulation as? SimulationResult.Success)?.data?.let { data ->
if (data is SimulationData.NoWalletChangesDetected) {
TextReference.Res(R.string.wc_no_wallet_changes_detected)
} else {
null
}
},
estimatedWalletChanges = (simulation as? SimulationResult.Success)?.data?.let { data ->
when (data) {
is SimulationData.Approve -> null
is SimulationData.Approve, SimulationData.NoWalletChangesDetected -> null
is SimulationData.SendAndReceive -> {
val items: ImmutableList<WcEstimatedWalletChangeUM> = (
data.send.map {
@ -84,7 +102,7 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor(
},
spendAllowance = (value.result.simulation as? SimulationResult.Success)?.data?.let { data ->
when (data) {
is SimulationData.SendAndReceive -> null
is SimulationData.SendAndReceive, SimulationData.NoWalletChangesDetected -> null
is SimulationData.Approve -> value.approvedAmount?.let {
spendAllowanceUMConverter.convert(it)
}

View file

@ -11,26 +11,42 @@ import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.notifications.Notification
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.resolveReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.entity.blockaid.BlockAidNotificationUM
@Composable
internal fun WcTransactionCheckErrorItem(notificationText: String, modifier: Modifier = Modifier) {
internal fun WcTransactionCheckErrorItem(notification: BlockAidNotificationUM, modifier: Modifier = Modifier) {
Notification(
modifier = modifier
.fillMaxWidth(),
config = NotificationConfig(
title = resourceReference(R.string.wc_malicious_transaction),
subtitle = TextReference.Str(notificationText),
iconResId = R.drawable.ic_alert_circle_24,
title = notification.title,
subtitle = TextReference.Str(notification.text?.resolveReference() ?: ""),
iconResId = when (notification.type) {
BlockAidNotificationUM.Type.ERROR -> R.drawable.ic_alert_circle_24
BlockAidNotificationUM.Type.WARNING -> R.drawable.ic_alert_triangle_20
},
),
containerColor = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
titleColor = TangemTheme.colors.text.warning,
subtitleColor = TangemTheme.colors.text.primary1,
iconTint = TangemTheme.colors.icon.warning,
containerColor = when (notification.type) {
BlockAidNotificationUM.Type.ERROR -> TangemColorPalette.Amaranth.copy(alpha = 0.1f)
BlockAidNotificationUM.Type.WARNING -> TangemColorPalette.Dark1.copy(alpha = 0.1f)
},
titleColor = when (notification.type) {
BlockAidNotificationUM.Type.ERROR -> TangemTheme.colors.text.warning
BlockAidNotificationUM.Type.WARNING -> TangemTheme.colors.text.primary1
},
subtitleColor = when (notification.type) {
BlockAidNotificationUM.Type.ERROR -> TangemTheme.colors.text.primary1
BlockAidNotificationUM.Type.WARNING -> TangemTheme.colors.text.tertiary
},
iconTint = when (notification.type) {
BlockAidNotificationUM.Type.ERROR -> TangemTheme.colors.icon.warning
BlockAidNotificationUM.Type.WARNING -> TangemTheme.colors.icon.attention
},
)
}
@ -43,7 +59,13 @@ private fun WcTransactionCheckErrorItemPreview() {
modifier = Modifier
.background(TangemTheme.colors.background.tertiary),
) {
WcTransactionCheckErrorItem("The transaction approves erc20 tokens to a known malicious address")
WcTransactionCheckErrorItem(
BlockAidNotificationUM(
type = BlockAidNotificationUM.Type.ERROR,
title = TextReference.Res(R.string.wc_malicious_transaction),
text = TextReference.Str("The transaction approves erc20 tokens to a known malicious address"),
),
)
}
}
}

View file

@ -5,20 +5,23 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.tooltip.TangemTooltip
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
@Composable
internal fun WcAddressItem(address: WcAddressUM, modifier: Modifier = Modifier) {
internal fun WcAddressItem(address: String, modifier: Modifier = Modifier) {
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
var isTooltipEnabled by remember { mutableStateOf(false) }
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(R.drawable.ic_user_square_24),
@ -26,25 +29,26 @@ internal fun WcAddressItem(address: WcAddressUM, modifier: Modifier = Modifier)
tint = TangemTheme.colors.icon.accent,
)
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.weight(1f),
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
text = stringResourceSafe(R.string.wc_common_address),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
)
SpacerWMax()
TangemTooltip(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing16)
.weight(1f),
text = address.fullAddress,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
text = address,
enabled = isTooltipEnabled,
content = { contentModifier ->
Text(
EllipsisText(
onTextLayout = { isTooltipEnabled = !it.hasVisualOverflow },
modifier = contentModifier,
text = address.shortAddress,
text = address,
textAlign = TextAlign.End,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
ellipsis = TextEllipsis.Middle,
)
},
)

View file

@ -1,16 +1,21 @@
package com.tangem.features.walletconnect.transaction.ui.common
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.tooltip.TangemTooltip
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.walletconnect.impl.R
@ -22,39 +27,46 @@ internal fun WcNetworkItem(networkInfo: WcNetworkInfoUM, modifier: Modifier = Mo
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
var isTooltipEnabled by remember { mutableStateOf(false) }
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(R.drawable.ic_network_new_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
)
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.weight(1f),
text = stringResourceSafe(R.string.wc_common_network),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End,
modifier = Modifier.weight(1f),
) {
Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = networkInfo.name,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
text = stringResourceSafe(R.string.wc_common_network),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
)
Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing6))
Image(
painter = painterResource(id = networkInfo.iconRes),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(CircleShape)
.size(TangemTheme.dimens.size20),
TangemTooltip(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
text = networkInfo.name,
enabled = isTooltipEnabled,
content = { contentModifier ->
Text(
modifier = contentModifier,
text = networkInfo.name,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onTextLayout = { isTooltipEnabled = it.hasVisualOverflow },
)
},
)
}
Image(
painter = painterResource(id = networkInfo.iconRes),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing6)
.clip(CircleShape)
.size(TangemTheme.dimens.size20),
)
}
}

View file

@ -15,23 +15,26 @@ import com.tangem.core.ui.components.divider.DividerWithPadding
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionFeeState
@Suppress("LongParameterList")
@Composable
internal fun WcSendTransactionItems(
walletName: String?,
networkInfo: WcNetworkInfoUM,
feeState: WcTransactionFeeState,
feeSelectorBlockComponent: FeeSelectorBlockComponent?,
address: WcAddressUM?,
feeSelectorUM: FeeSelectorUM,
address: String?,
modifier: Modifier = Modifier,
) {
val onFeeBlockClicked = remember(feeState) {
when (feeState) {
WcTransactionFeeState.None -> null
is WcTransactionFeeState.Success -> feeState.onClick
val onFeeBlockClicked = remember(feeState, feeSelectorUM) {
if (feeState is WcTransactionFeeState.Success && feeSelectorUM is FeeSelectorUM.Content) {
feeState.onClick
} else {
null
}
}
Column(

View file

@ -20,6 +20,7 @@ internal fun WcTransactionRequestButtons(
onDismiss: () -> Unit,
onClickActiveButton: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
SecondaryButton(
@ -37,6 +38,7 @@ internal fun WcTransactionRequestButtons(
onClick = onClickActiveButton,
iconResId = R.drawable.ic_tangem_24,
showProgress = isLoading,
enabled = enabled,
)
}
}

View file

@ -5,11 +5,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.tooltip.TangemTooltip
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.walletconnect.impl.R
@ -17,6 +19,7 @@ import com.tangem.features.walletconnect.impl.R
@Composable
internal fun WcWalletItem(walletName: String, modifier: Modifier = Modifier) {
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
var isTooltipEnabled by remember { mutableStateOf(false) }
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(R.drawable.ic_wallet_new_24),
@ -24,21 +27,28 @@ internal fun WcWalletItem(walletName: String, modifier: Modifier = Modifier) {
tint = TangemTheme.colors.icon.accent,
)
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.weight(1f),
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
)
Text(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing16)
.weight(1f),
SpacerWMax()
TangemTooltip(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
text = walletName,
textAlign = TextAlign.End,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
enabled = isTooltipEnabled,
content = { contentModifier ->
Text(
modifier = contentModifier,
text = walletName,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onTextLayout = { isTooltipEnabled = it.hasVisualOverflow },
)
},
)
}
}

View file

@ -4,7 +4,9 @@ import android.content.res.Configuration
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -14,25 +16,29 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter
import com.tangem.core.ui.components.divider.DividerWithPadding
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState
import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem
import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.components.PreviewFeeSelectorBlockComponent
import com.tangem.features.walletconnect.transaction.entity.blockaid.BlockAidNotificationUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionFeeState
@ -49,6 +55,7 @@ import kotlinx.collections.immutable.persistentListOf
internal fun WcSendTransactionModalBottomSheet(
state: WcSendTransactionItemUM,
feeSelectorBlockComponent: FeeSelectorBlockComponent?,
feeSelectorUM: FeeSelectorUM,
onClickTransactionRequest: () -> Unit,
onBack: () -> Unit,
onDismiss: () -> Unit,
@ -101,14 +108,23 @@ internal fun WcSendTransactionModalBottomSheet(
if (state.estimatedWalletChanges != null) {
TransactionCheckResultsItem(state.estimatedWalletChanges, onClickAllowToSpend)
}
Spacer(Modifier.height(16.dp))
WcSendTransactionItems(
modifier = Modifier.padding(top = 16.dp),
walletName = state.walletName,
networkInfo = state.networkInfo,
feeState = state.feeState,
feeSelectorBlockComponent = feeSelectorBlockComponent,
feeSelectorUM = feeSelectorUM,
address = state.address,
)
if (state.feeErrorNotification != null) {
Notification(
modifier = Modifier.padding(top = 14.dp),
config = state.feeErrorNotification.config,
iconTint = TangemTheme.colors.icon.warning,
containerColor = TangemTheme.colors.button.disabled,
)
}
}
}
},
@ -119,6 +135,7 @@ internal fun WcSendTransactionModalBottomSheet(
onClickActiveButton = state.onSend,
activeButtonText = resourceReference(R.string.common_send),
isLoading = state.isLoading,
enabled = state.sendEnabled,
)
},
)
@ -155,6 +172,7 @@ private fun WcSendTransactionBottomSheetPreview(
onBack = {},
onDismiss = {},
onClickAllowToSpend = {},
feeSelectorUM = FeeSelectorUM.Loading,
)
},
)
@ -173,8 +191,10 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide
appSubtitle = "react-app.walletconnect.com",
),
estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM(
notificationText = TextReference.Str(
"The transaction approves erc20 tokens to a known malicious address",
notification = BlockAidNotificationUM(
type = BlockAidNotificationUM.Type.ERROR,
title = TextReference.Res(R.string.wc_malicious_transaction),
text = TextReference.Str("The transaction approves erc20 tokens to a known malicious address"),
),
estimatedWalletChanges = WcEstimatedWalletChangesUM(
items = persistentListOf(
@ -194,10 +214,12 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide
),
isLoading = false,
),
walletName = "Tangem 2.0",
networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22),
feeState = WcTransactionFeeState.Success(null, {}),
walletName = "Tangem 2.0 Tangem 2.0 Tangem 2",
networkInfo = WcNetworkInfoUM(name = "Optimistic Ethereum Network", iconRes = R.drawable.img_eth_22),
feeState = WcTransactionFeeState.Success(dAppFee = null, onClick = {}),
address = null,
sendEnabled = true,
feeErrorNotification = null,
),
WcSendTransactionItemUM(
onDismiss = {},
@ -209,8 +231,10 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide
appSubtitle = "react-app.walletconnect.com",
),
estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM(
notificationText = TextReference.Str(
"The transaction approves erc20 tokens to a known malicious address",
notification = BlockAidNotificationUM(
type = BlockAidNotificationUM.Type.ERROR,
title = TextReference.Res(R.string.wc_malicious_transaction),
text = TextReference.Str("The transaction approves erc20 tokens to a known malicious address"),
),
estimatedWalletChanges = WcEstimatedWalletChangesUM(
items = persistentListOf(
@ -231,8 +255,13 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide
),
walletName = "Tangem 2.0",
networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22),
feeState = WcTransactionFeeState.Success(null, {}),
address = WcAddressUM("0xdac17f958d2ee523a2206206994597c13d831ec7", "0x345FF...34FA"),
feeState = WcTransactionFeeState.Success(dAppFee = null, onClick = {}),
address = "0xdac17f958d2ee523a2206206994597c13d831ec7",
sendEnabled = true,
feeErrorNotification = NotificationUM.Info(
title = stringReference("Insufficient Ethereum"),
subtitle = stringReference("Top up your balance to cover the network fee"),
),
),
WcSendTransactionItemUM(
onDismiss = {},
@ -244,8 +273,10 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide
appSubtitle = "react-app.walletconnect.com",
),
estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM(
notificationText = TextReference.Str(
"The transaction approves erc20 tokens to a known malicious address",
notification = BlockAidNotificationUM(
type = BlockAidNotificationUM.Type.ERROR,
title = TextReference.Res(R.string.wc_malicious_transaction),
text = TextReference.Str("The transaction approves erc20 tokens to a known malicious address"),
),
estimatedWalletChanges = WcEstimatedWalletChangesUM(
items = persistentListOf(
@ -268,6 +299,11 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide
networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22),
feeState = WcTransactionFeeState.None,
address = null,
sendEnabled = false,
feeErrorNotification = NotificationUM.Info(
title = stringReference("Insufficient Ethereum"),
subtitle = stringReference("Top up your balance to cover the network fee"),
),
),
),
)

View file

@ -28,7 +28,6 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState
import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem
import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.entity.common.WcAddressUM
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionItemUM
@ -196,7 +195,7 @@ private class WcSignTransactionStateProvider : CollectionPreviewParameterProvide
),
walletName = null,
networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22),
address = WcAddressUM("0xdac17f958d2ee523a2206206994597c13d831ec7", "0x345FF...34FA"),
address = "0xdac17f958d2ee523a2206206994597c13d831ec7",
),
),
)

View file

@ -0,0 +1,65 @@
package com.tangem.features.walletconnect.utils
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils
import com.tangem.features.walletconnect.impl.R
import javax.inject.Inject
internal class WcNotificationsFactory @Inject constructor() {
fun createFeeNotifications(
cryptoCurrencyStatus: CryptoCurrencyStatus,
feeSelectorUM: FeeSelectorUM?,
onFeeReload: () -> Unit,
): NotificationUM.Info? {
return when (feeSelectorUM) {
is FeeSelectorUM.Content -> createFeeExceedsBalance(cryptoCurrencyStatus, feeSelectorUM)
is FeeSelectorUM.Error -> createFeeErrorNotification(onFeeReload)
FeeSelectorUM.Loading, null -> null
}
}
private fun createFeeExceedsBalance(
cryptoCurrencyStatus: CryptoCurrencyStatus,
feeSelectorUM: FeeSelectorUM?,
): NotificationUM.Info? {
// TODO: [REDACTED_TASK_KEY] localization
return NotificationUM.Info(
title = stringReference("Insufficient ${cryptoCurrencyStatus.currency.name}"),
subtitle = stringReference("Top up your balance to cover the network fee"),
).takeIf { isFeeExceedsBalance(cryptoCurrencyStatus = cryptoCurrencyStatus, feeSelectorUM = feeSelectorUM) }
}
private fun createFeeErrorNotification(onFeeReload: () -> Unit): NotificationUM.Info {
return NotificationUM.Info(
title = resourceReference(R.string.send_fee_unreachable_error_title),
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_button_refresh),
onClick = onFeeReload,
),
)
}
private fun isFeeExceedsBalance(
cryptoCurrencyStatus: CryptoCurrencyStatus,
feeSelectorUM: FeeSelectorUM?,
): Boolean {
val feeSelectorContent = feeSelectorUM as? FeeSelectorUM.Content ?: return false
val lowestFee = when (val fees = feeSelectorContent.fees) {
is TransactionFee.Choosable -> fees.minimum
is TransactionFee.Single -> fees.normal
}
return FeeCalculationUtils.checkExceedBalance(
feeBalance = cryptoCurrencyStatus.value.amount,
feeAmount = lowestFee.amount.value,
)
}
}