Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-21 18:57:09 +04:00
commit 3e70beab28
103 changed files with 1908 additions and 626 deletions

@ -1 +1 @@
Subproject commit 498b0bcd0d871ed60c43b5d44f646548a4f11d37
Subproject commit 158fbd8808d2db92ef82d3f9ed92c81340c707c5

View file

@ -111,6 +111,7 @@ sealed class AnalyticsParam {
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
override val feeAssetType: FeeAssetType,
) : TxSentFrom("Send"), TxData
data class Swap(
@ -118,6 +119,7 @@ sealed class AnalyticsParam {
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
override val feeAssetType: FeeAssetType,
) : TxSentFrom("Swap"), TxData
data class Staking(
@ -125,6 +127,7 @@ sealed class AnalyticsParam {
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
override val feeAssetType: FeeAssetType,
) : TxSentFrom("Staking"), TxData
data class Approve(
@ -132,6 +135,7 @@ sealed class AnalyticsParam {
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
override val feeAssetType: FeeAssetType,
val permissionType: String,
) : TxSentFrom("Approve"), TxData
@ -140,6 +144,7 @@ sealed class AnalyticsParam {
override val token: String,
override val feeType: FeeType?,
override val feeToken: String,
override val feeAssetType: FeeAssetType,
) : TxSentFrom("WalletConnect"), TxData
data object Sell : TxSentFrom("Sell")
@ -149,6 +154,7 @@ sealed class AnalyticsParam {
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
override val feeAssetType: FeeAssetType,
) : TxSentFrom("NFT"), TxData
data class SendWithSwap(
@ -156,6 +162,7 @@ sealed class AnalyticsParam {
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
override val feeAssetType: FeeAssetType,
) : TxSentFrom("Send&Swap"), TxData
data class Earning(
@ -163,6 +170,7 @@ sealed class AnalyticsParam {
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
override val feeAssetType: FeeAssetType,
) : TxSentFrom("Earning"), TxData
}
@ -171,6 +179,12 @@ sealed class AnalyticsParam {
val token: String
val feeToken: String
val feeType: FeeType?
val feeAssetType: FeeAssetType
}
sealed class FeeAssetType(val value: String) {
data object Coin : FeeAssetType("Coin")
data object Token : FeeAssetType("Token")
}
sealed class FeeType(val value: String) {
@ -307,6 +321,8 @@ sealed class AnalyticsParam {
const val REFERRAL_ID = "Referral_ID"
const val SEARCHED = "Searched"
const val RATE_TYPE = "Rate Type"
const val SCREEN_TYPE = "Screen Type"
const val FEE_ASSET_TYPE = "Fee Asset Type"
const val SIGN_IN_TYPE = "Sign in type"
const val WALLETS_COUNT = "Wallets Count"
const val WALLET_TYPE = "Wallet Type"

View file

@ -77,6 +77,7 @@ sealed class Basic(
put(AnalyticsParam.TOKEN_PARAM, sentFrom.token)
sentFrom.feeType?.value?.let { put(AnalyticsParam.FEE_TYPE, it) }
put(AnalyticsParam.FEE_TOKEN, sentFrom.feeToken)
put(AnalyticsParam.FEE_ASSET_TYPE, sentFrom.feeAssetType.value)
}
if (sentFrom is AnalyticsParam.TxSentFrom.Approve) {
put(AnalyticsParam.PERMISSION_TYPE, sentFrom.permissionType)

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.DefaultTangemPayReissueCardStore
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
@ -25,9 +26,10 @@ internal object TangemPayStoresModule {
@Provides
@Singleton
fun provideTangemPayReissueCardStore(): TangemPayReissueCardStore {
fun provideTangemPayReissueCardStore(prefs: AppPreferencesStore): TangemPayReissueCardStore {
return DefaultTangemPayReissueCardStore(
feeStore = RuntimeDataStore(),
prefs = prefs,
)
}
}

View file

@ -1,11 +1,16 @@
package com.tangem.datasource.local.visa
import androidx.datastore.preferences.core.stringPreferencesKey
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.models.wallet.UserWalletId
internal class DefaultTangemPayReissueCardStore(
private val feeStore: RuntimeDataStore<TangemPayReissueCardFee>,
private val prefs: AppPreferencesStore,
) : TangemPayReissueCardStore {
override suspend fun storeReissueFee(
@ -20,11 +25,15 @@ internal class DefaultTangemPayReissueCardStore(
}
override suspend fun storeReissueOrderId(cardId: String, orderId: String) {
// TODO v_rodionov: #[REDACTED_TASK_KEY] store orderId in app prefs
prefs.store(
key = getReissueKey(cardId),
value = orderId,
)
}
override suspend fun getOrderId(cardId: String): String? {
// TODO v_rodionov: #[REDACTED_TASK_KEY] store orderId in app prefs
return null
return prefs.getSyncOrNull(key = getReissueKey(cardId))
}
private fun getReissueKey(cardId: String) = stringPreferencesKey("tangem_pay_reissue_card_$cardId")
}

View file

@ -45,6 +45,7 @@ sealed interface PaymentAccountStatusValueDM {
@Json(name = "deposit_address") val depositAddress: String?,
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
@Json(name = "available_for_withdrawal") val availableForWithdrawal: BigDecimal,
@Json(name = "cards") val cards: List<TangemPayCard>,
) : PaymentAccountStatusValueDM
@ -85,5 +86,6 @@ sealed interface PaymentAccountStatusValueDM {
@Json(name = "admin_daily_limit") val adminDailyLimit: SerializedBigDecimal?,
@Json(name = "is_frozen") val isFrozen: Boolean,
@Json(name = "last_digits") val lastDigits: String,
@Json(name = "is_reissuing") val isReissuing: Boolean,
)
}

View file

@ -80,6 +80,9 @@
<string name="action_buttons_swap_not_enough_tokens_alert_title">Token hinzufügen</string>
<string name="action_buttons_you_want_to_receive">Wähle den Token aus, den Du erhalten möchtest</string>
<string name="action_buttons_you_want_to_swap">Wähle den Token, den Du tauschen möchtest</string>
<string name="actionbutton_addfunds_title">Guthaben hinzufügen</string>
<string name="actionbutton_swap_title">Tauschen</string>
<string name="actionbutton_transfer_title">Übertragung</string>
<string name="add_and_manage_sheet_manage_subtitle">Zum Portfolio hinzufügen</string>
<string name="add_and_manage_sheet_manage_title">Token hinzufügen</string>
<string name="add_and_manage_sheet_organize_subtitle">Sortieren und Gruppieren</string>
@ -87,6 +90,10 @@
<string name="add_custom_token_choose_network">Netzwerk wählen</string>
<string name="add_custom_token_title">Token anlegen</string>
<string name="add_tokens_title">Token verwalten</string>
<string name="addfunds_buy_row_description">Kreditkarte oder Bankkonto</string>
<string name="addfunds_receive_row_description">Teile deine Adresse oder dein QR-Code</string>
<string name="addfunds_swap_row_description">Zwische deinen Portfolios</string>
<string name="addfunds_you_receive_title">Empfangen</string>
<string name="address_qr_code_message_format">Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen.</string>
<string name="address_type_default">Standard</string>
<string name="address_type_legacy">Altbestand</string>
@ -433,6 +440,7 @@
<string name="common_transactions">Transaktionen</string>
<string name="common_transfer">Überweisung</string>
<string name="common_transferred">Übertragen</string>
<string name="common_try_again_later">Bitte versuche es später noch einmal.</string>
<string name="common_unable_to_load">Die Daten konnten nicht geladen werden…</string>
<string name="common_understand">Ich verstehe</string>
<string name="common_understand_continue">Ich verstehe, fahre bitte fort.</string>
@ -1063,6 +1071,7 @@
<string name="onboarding_create_wallet_options_button_options">Andere Optionen</string>
<string name="onboarding_create_wallet_options_message">Deine Schlüssel(private-keys) werden sicher im Inneren der Karte oder Ring generiert. Es gibt keine Seed-Phrase, d. h. niemand kann sie exportieren oder stehlen.</string>
<string name="onboarding_create_wallet_options_title">Schlüssel anonym generieren</string>
<string name="onboarding_create_wallet_term_of_conditions_text">Indem Sie fortfahren, stimmen Sie den folgenden Bedingungen zu:</string>
<string name="onboarding_done_body">Deine Karte oder Ring ist aktiviert und einsatzbereit</string>
<string name="onboarding_done_header">Erfolgreich!</string>
<string name="onboarding_done_wallet">Deine Wallet ist eingerichtet und einsatzbereit!</string>
@ -1188,6 +1197,8 @@
<string name="onramp_via_provider">über %s</string>
<string name="onramp_you_will_pay_title">Du zahlst</string>
<string name="organize_tokens_group">Gruppe erstellen</string>
<string name="organize_tokens_menu_group_by_networks">Netzwerke auswählen</string>
<string name="organize_tokens_menu_sort_by_balance">Nach Kontostand sortieren</string>
<string name="organize_tokens_sort_by_balance">Nach Guthaben</string>
<string name="organize_tokens_title">Token organisieren</string>
<string name="organize_tokens_ungroup">Gruppe löschen</string>
@ -1682,7 +1693,7 @@
<string name="tangempay_card_details_change_pin_success_title">PIN-Code erstellt</string>
<string name="tangempay_card_details_cvc">CVC</string>
<string name="tangempay_card_details_error_text">Daten konnten nicht geladen werden. Versuche es später noch einmal.</string>
<string name="tangempay_card_details_expiry">Ablaufdatum</string>
<string name="tangempay_card_details_expiry">Ablauf</string>
<string name="tangempay_card_details_freeze_card">Karte einfrieren</string>
<string name="tangempay_card_details_hide_details">Details ausblenden</string>
<string name="tangempay_card_details_hide_text">Verstecken</string>
@ -1746,8 +1757,15 @@
<string name="tangempay_get_banner_description">Holen Sie sich Ihre kostenlose virtuelle Tangem Visa-Karte</string>
<string name="tangempay_get_tangem_pay">Tangem Pay erhalten</string>
<string name="tangempay_go_to_support">Zum Support</string>
<string name="tangempay_issue_additional_card_description">Es generiert einen neuen Satz Kartendaten.</string>
<string name="tangempay_issue_additional_card_fee_label">Ausgabegebühr</string>
<string name="tangempay_issue_additional_card_insufficient_funds_subtitle">Einzahlung von USDC auf das Zahlungskonto zur Deckung der Ausstellungsgebühr</string>
<string name="tangempay_issue_additional_card_insufficient_funds_title">Gebühr kann nicht gedeckt werden</string>
<string name="tangempay_issue_additional_card_title">Eine zusätzliche Karte ausstellen?</string>
<string name="tangempay_issue_card">Karte ausstellen</string>
<string name="tangempay_issue_card_notification_description">Es dauert in der Regel bis zu 15 Minuten.</string>
<string name="tangempay_issue_card_notification_title">Einrichtung Ihrer Tangem-Karte</string>
<string name="tangempay_issuing_new_digital_card_title">Ausstellung einer neuen digitalen Karte</string>
<string name="tangempay_issuing_your_card">Ausstellung Deiner Karte</string>
<string name="tangempay_issuing_your_card_description">Die Karte wird in der Regel innerhalb von 5 Minuten automatisch ausgestellt. In seltenen Fällen, wenn eine manuelle Überprüfung erforderlich ist, kann die Ausstellung bis zu 48 Stunden dauern.</string>
<string name="tangempay_kyc_card_ready_notification_title">Tangem Pay</string>
@ -1764,6 +1782,8 @@
<string name="tangempay_kyc_rejected_button_text">KYC-Sperre ausblenden</string>
<string name="tangempay_kyc_rejected_description">Leider konnten wir Ihre Identität</string>
<string name="tangempay_kyc_rejected_description_span">nicht verifizieren.</string>
<string name="tangempay_maximum_cards_issued_description">Du kannst bis zu 3 Karten haben. Lösche eine, um eine neue Karte hinzuzufügen.</string>
<string name="tangempay_maximum_cards_issued_title">Maximale Anzahl ausgegebener Karten</string>
<string name="tangempay_onboarding_banner_description">Holen Sie sich Ihre kostenlose virtuelle Tangem Visa-Karte</string>
<string name="tangempay_onboarding_banner_title">Nutzen Sie USDC für alltägliche Zahlungen</string>
<string name="tangempay_onboarding_get_card_button_text">Karte erhalten</string>
@ -1872,6 +1892,7 @@
<string name="transaction_history_transaction_validator">Validierer: %s</string>
<string name="transaction_notifications_warning_description">Benachrichtigungen sind aktiviert, funktionieren aber erst, wenn Du Benachrichtigungen in Deinen Geräteeinstellungen zulässt.</string>
<string name="transaction_notifications_warning_title">Transaktionsbenachrichtigungen</string>
<string name="transfer_in_progress_title">Transfer läuft</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Der Mindesttransaktionsbetrag beträgt %1$s.</string>
<string name="tron_will_be_send_token_fee_description">Die Netzwerkgebühren für beliebte Token im Tron-Netzwerk können höher sein. Das Staking von TRX kann helfen, die Transaktionskosten zu senken.</string>
@ -1903,7 +1924,9 @@
<string name="user_push_notification_agreement_argument_three">Genehmigung für Transaktionen anfordern</string>
<string name="user_push_notification_agreement_argument_two">Sei der Erste, der von neuen Aktionen erfährt</string>
<string name="user_push_notification_agreement_argument_two_subtitle">Frühzeitiger Zugriff auf neue Funktionen und exklusive Angebote.</string>
<string name="user_push_notification_agreement_argument_two_subtitle_v2">Preisbenachrichtigungen, Produktneuheiten und exklusive Angebote</string>
<string name="user_push_notification_agreement_argument_two_title">Updates zu Funktionen und Neuigkeiten</string>
<string name="user_push_notification_agreement_argument_two_title_v2">Angebote &amp; Updates</string>
<string name="user_push_notification_agreement_header">Möchtest du Push-Benachrichtigungen verwenden?</string>
<string name="user_push_notification_banner_subtitle">Aktiviere Push-Benachrichtigungen und wir benachrichtigen Dich sofort, wenn Gelder eintreffen</string>
<string name="user_push_notification_banner_title">Verpasse keine Transaktion</string>

View file

@ -646,6 +646,8 @@
<string name="hw_backup_banner_description">Mantiene sus criptomonedas seguras y sin conexión. Tan delgadas como una tarjeta de crédito, más seguras que una bóveda bancaria.</string>
<string name="hw_backup_close_description">Si lo hace, tendrá que empezar de nuevo.</string>
<string name="hw_backup_google_drive_description">Recuperar la billetera existente a través de una copia de seguridad de Google Drive</string>
<string name="hw_backup_google_drive_dialog_message">Estamos trabajando en la copia de seguridad de Google Drive para facilitar aún más la recuperación de la billetera.</string>
<string name="hw_backup_google_drive_dialog_title">Próximamente, copia de seguridad de Google Drive</string>
<string name="hw_backup_google_drive_title">Copia de seguridad de Google Drive</string>
<string name="hw_backup_hardware_create_description">Cree una billetera segura y transfiera sus fondos para mayor protección.</string>
<string name="hw_backup_hardware_create_title">Crear nueva billetera</string>
@ -985,6 +987,11 @@
<string name="onboarding_activation_error_message">Por favor repita la operación. La tarjeta/anillo se restablecerá a la configuración de fábrica.</string>
<string name="onboarding_activation_error_title">Error de activación</string>
<string name="onboarding_add_tokens">Agregar tokens</string>
<plurals name="onboarding_address_sync_description">
<item quantity="one">Su wallet contiene %d token. Para continuar, por favor sincronice sus direcciones.</item>
<item quantity="other">Su wallet contiene %d tokens. Para continuar, por favor sincronice sus direcciones.</item>
</plurals>
<string name="onboarding_address_sync_title">Sincroniza tu wallet</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">Ha añadido una tarjeta o anillo como copia de seguridad. Una vez finalizada la copia de seguridad, no puedes añadir más dispositivos. Si tiene una tarjeta o anillo más, añádalo ahora. ¿Quiere continuar?</string>
<string name="onboarding_backup_exit_warning">La copia de seguridad está parcialmente completa y no se puede salir ahora.</string>
<string name="onboarding_bottom_sheet_passphrase_description">Una frase de contraseña es una función de seguridad opcional que añade una palabra o frase a su frase de recuperación, creando un nuevo conjunto de direcciones de billetera para una mayor protección.</string>
@ -1006,6 +1013,7 @@
<string name="onboarding_create_wallet_options_button_options">Otras opciones</string>
<string name="onboarding_create_wallet_options_message">Sus claves se generarán de forma segura dentro del chip. No hay seed phrase, lo que significa que nadie puede exportarla ni robarla.</string>
<string name="onboarding_create_wallet_options_title">Generar claves de forma privada</string>
<string name="onboarding_create_wallet_term_of_conditions_text">Al continuar, acepta las \n%s</string>
<string name="onboarding_done_body">Su tarjeta está activada y lista para usar</string>
<string name="onboarding_done_header">¡Éxito!</string>
<string name="onboarding_done_wallet">¡Su billetera está configurada y lista para usar!</string>
@ -1510,13 +1518,21 @@
<string name="swap_story_fifth_subtitle">Siéntase seguro con una asistencia permanente que le ayudará con cualquier problema</string>
<string name="swap_story_fifth_title">Siempre aquí</string>
<string name="swap_story_first_subtitle">Múltiples proveedores de confianza en un solo lugar: intercambie cualquier activo sin esfuerzo en su billetera</string>
<string name="swap_story_first_subtitle_v2">Intercambie criptos directamente en Tangem\nSin transferencias adicionales\nSin mover fondos a exchanges</string>
<string name="swap_story_first_title">Intercambie con nosotros</string>
<string name="swap_story_first_title_v2">Intercambia dentro de tu billetera</string>
<string name="swap_story_forth_subtitle">Sin cuelgues, sin pérdidas, sin puntos ciegos - su transacción siempre está protegida</string>
<string name="swap_story_forth_subtitle_v2">Los intercambios se realizan a través de proveedores de confianza. Sus llaves permanecen en su billetera Tangem en todo momento. Claro. Transparente. Autocustodiado.</string>
<string name="swap_story_forth_title">Defensa impenetrable</string>
<string name="swap_story_forth_title_v2">Ud mantiene el control</string>
<string name="swap_story_second_subtitle">Maximice su valor con tarifas de una amplia red de proveedores de confianza, eligiendo siempre la mejor</string>
<string name="swap_story_second_subtitle_v2">Tangem compara múltiples proveedores, tanto DEX como CEX. La mejor tarifa se selecciona automáticamente. ¿Prefiere otro proveedor? Puede elegirlo manualmente.</string>
<string name="swap_story_second_title">Tarifas inmejorables</string>
<string name="swap_story_second_title_v2">Mejor tarifa disponible</string>
<string name="swap_story_third_subtitle">Sencillo e intuitivo, permite cambiar tokens con solo unos cuantos toques</string>
<string name="swap_story_third_subtitle_v2">Intercambie entre las principales redes y miles de tokens 0% Comisión por intercambios entre stablecoins</string>
<string name="swap_story_third_title">Simplemente cómodo</string>
<string name="swap_story_third_title_v2">90+ Blockchains\n16.000+ Activos</string>
<string name="swap_via_provider">Intercambio a través del proveedor</string>
<string name="swap_your_assets_title">Sus activos</string>
<string name="swapping_alert_cex_description">La cantidad incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario.</string>
@ -1589,7 +1605,7 @@
<string name="tangempay_card_details_change_pin_success_title">Código PIN creado</string>
<string name="tangempay_card_details_cvc">CVC</string>
<string name="tangempay_card_details_error_text">Error al cargar datos. Inténtalo de nuevo más tarde.</string>
<string name="tangempay_card_details_expiry">Caducidad</string>
<string name="tangempay_card_details_expiry">Expira</string>
<string name="tangempay_card_details_freeze_card">Congelar tarjeta</string>
<string name="tangempay_card_details_hide_details">Ocultar detalles</string>
<string name="tangempay_card_details_hide_text">Ocultar</string>
@ -2030,6 +2046,10 @@
<item quantity="one">Use su tarjeta o anillo para obtener una dirección para la red %d</item>
<item quantity="other">Use su tarjeta o anillo para obtener direcciónes para las redes %d</item>
</plurals>
<plurals name="warning_missing_derivation_no_nfc_message">
<item quantity="one">Sincronice direcciones para obtener una dirección de la red %d</item>
<item quantity="other">Sincronice direcciones para obtener direcciones de las redes %d</item>
</plurals>
<string name="warning_missing_derivation_title">Faltan algunas direcciones</string>
<string name="warning_network_unreachable_message">La red no está disponible actualmente. Por favor, inténtalo de nuevo más tarde.</string>
<string name="warning_network_unreachable_title">La red no está disponible</string>

View file

@ -699,6 +699,9 @@
<string name="initial_message_scan_header">Posez pour scanner</string>
<string name="initial_message_sign_header">Tapez pour signer</string>
<string name="initial_message_tap_header">Posez la carte</string>
<string name="initial_wallet_sync_banner_description">Votre wallet est synchronisé et prêt.\nDes tokens sont manquants ?</string>
<string name="initial_wallet_sync_banner_title">Wallet importé avec succès</string>
<string name="initial_wallet_sync_restore_progress">Restauration %d%%</string>
<string name="key_invalidated_warning_description">Vous avez mis à jour vos données biométriques, scannez votre carte ou bague pour entrer</string>
<string name="koinos_insufficient_balance_to_send_koin_description">Votre solde doit être supérieur à la valeur des frais pour effectuer un transfert</string>
<string name="koinos_insufficient_balance_to_send_koin_title">Solde insuffisant</string>
@ -947,6 +950,7 @@
<string name="onboarding_activation_error_message">Veuillez répéter l\'opération. La carte sera réinitialisée aux paramètres d\'usine.</string>
<string name="onboarding_activation_error_title">Erreur d\'activation</string>
<string name="onboarding_add_tokens">Ajouter des jetons</string>
<string name="onboarding_address_sync_title">Synchronisez votre wallet</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">Vous avez ajouté une carte de sauvegarde. Une fois le processus de sauvegarde terminé, vous ne pourrez plus ajouter de cartes de sauvegarde. Si vous avez une autre carte, ajoutez-la à la sauvegarde. Souhaitez-vous poursuivre le processus de sauvegarde ?</string>
<string name="onboarding_backup_exit_warning">Le processus de sauvegarde est partiellement terminé. Vous ne pouvez pas le quitter maintenant.</string>
<string name="onboarding_bottom_sheet_passphrase_description">La phrase secrète est une fonctionnalité de sécurité avancée utilisée par les portefeuilles cryptographiques. Il ajoute un mot ou une phrase supplémentaire de votre choix à votre phrase de récupération déjà existante pour débloquer un tout nouvel ensemble d\'adresses.</string>
@ -1461,13 +1465,21 @@
<string name="swap_story_fifth_subtitle">Ayez confiance en notre assistance 24 heures sur 24 pour vous aider à résoudre tous vos problèmes</string>
<string name="swap_story_fifth_title">Assistance 24 heures sur 24</string>
<string name="swap_story_first_subtitle">Plusieurs fournisseurs de confiance en un seul endroit : échangez n\'importe quel actif facilement</string>
<string name="swap_story_first_subtitle_v2">Échangez des cryptos directement dans Tangem\nSans transferts supplémentaires\nSans envoyer vos fonds vers un échange</string>
<string name="swap_story_first_title">Échangez Avec Nous</string>
<string name="swap_story_first_title_v2">Échangez Dans Votre Portefeuille</string>
<string name="swap_story_forth_subtitle">Une sécurité de haut niveau et des fournisseurs vérifiés garantissant que vos actifs sont protégés à chaque échange</string>
<string name="swap_story_forth_subtitle_v2">Les échanges sont effectués par des prestataires de confiance. Vos clefs ne quittent jamais votre portefeuille Tangem. Clair. Transparent. Gestion autonome</string>
<string name="swap_story_forth_title">Plus Sûr Que Jamais</string>
<string name="swap_story_forth_title_v2">Vous Gardez le Contrôle</string>
<string name="swap_story_second_subtitle">Maximisez votre valeur avec des tarifs d\'un large réseau de fournisseurs de confiance</string>
<string name="swap_story_second_subtitle_v2">Tangem compare plusieurs fournisseurs, DEX et CEX. Le meilleur tarif est sélectionné automatiquement. Vous préférez un autre fournisseur? Vous pouvez le choisir manuellement.</string>
<string name="swap_story_second_title">Meilleurs Tarifs</string>
<string name="swap_story_second_title_v2">Meilleur Tarif Disponible</string>
<string name="swap_story_third_subtitle">Simple et intuitif, vous permettant d\'échanger des jetons en quelques clics</string>
<string name="swap_story_third_subtitle_v2">Échangez des milliers de jetons sur les principaux réseaux 0% de frais sur les échanges stablecoin à stablecoin</string>
<string name="swap_story_third_title">Plus Simple Que Jamais</string>
<string name="swap_story_third_title_v2">90+ Blockchains\n16,000+ Actifs</string>
<string name="swap_via_provider">Échange via le fournisseur</string>
<string name="swap_your_assets_title">Vos actifs</string>
<string name="swapping_alert_cex_description">Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %s depuis l\'échange vers l\'adresse de l\'utilisateur.</string>

View file

@ -80,6 +80,9 @@
<string name="action_buttons_swap_not_enough_tokens_alert_title">トークンの追加</string>
<string name="action_buttons_you_want_to_receive">受け取りたいトークンを選択します</string>
<string name="action_buttons_you_want_to_swap">スワップしたいトークンを選択します</string>
<string name="actionbutton_addfunds_title">資金を追加</string>
<string name="actionbutton_swap_title">スワップ</string>
<string name="actionbutton_transfer_title">送金</string>
<string name="add_and_manage_sheet_manage_subtitle">ポートフォリオに追加</string>
<string name="add_and_manage_sheet_manage_title">トークンを追加</string>
<string name="add_and_manage_sheet_organize_subtitle">並べ替え・グループ化</string>
@ -87,6 +90,10 @@
<string name="add_custom_token_choose_network">ネットワークを選択</string>
<string name="add_custom_token_title">カスタムトークンの追加</string>
<string name="add_tokens_title">トークンの管理</string>
<string name="addfunds_buy_row_description">クレジットカードまたは銀行口座</string>
<string name="addfunds_receive_row_description">アドレスまたはQRコードを共有</string>
<string name="addfunds_swap_row_description">自分のポートフォリオ間で</string>
<string name="addfunds_you_receive_title">受け取る</string>
<string name="address_qr_code_message_format">このアドレスには%3$s ネットワークから%1$s (%2$s) のみを送信してください。他のトークンやネットワークを使用すると、資金を失う可能性があります。</string>
<string name="address_type_default">デフォルト</string>
<string name="address_type_legacy">レガシー</string>
@ -1046,6 +1053,7 @@
<string name="onboarding_create_wallet_options_button_options">その他のオプション</string>
<string name="onboarding_create_wallet_options_message">秘密鍵はチップ内で安全に生成されます。シードフレーズは存在しないので、誰もエクスポートしたり盗んだりすることはできません。</string>
<string name="onboarding_create_wallet_options_title">秘密鍵を非公開で生成する</string>
<string name="onboarding_create_wallet_term_of_conditions_text">続行すると、以下に同意したものとみなされます。</string>
<string name="onboarding_done_body">カードは有効化され、使用可能になりました</string>
<string name="onboarding_done_header">成功!</string>
<string name="onboarding_done_wallet">ウォレットの設定が完了し、使用できるようになりました。</string>
@ -1564,13 +1572,13 @@
<string name="swap_story_fifth_subtitle">24時間体制のサポートであらゆる問題に対応します。</string>
<string name="swap_story_fifth_title">いつもここに</string>
<string name="swap_story_first_subtitle">複数の信頼できるプロバイダーが一箇所に集結。ウォレット内で様々な暗号資産を簡単に交換できます。</string>
<string name="swap_story_first_subtitle_v2">Tangem内で暗号資産を直接交換できます\n追加の送金は不要です\n取引所に資金を移す必要もありません</string>
<string name="swap_story_first_subtitle_v2">Tangem内で暗号資産を直接交換できます\n追加の送金は不要\n取引所に資金を移す必要もありません</string>
<string name="swap_story_first_title">ぜひスワップしてください</string>
<string name="swap_story_first_title_v2">ウォレット内でスワップ</string>
<string name="swap_story_forth_subtitle">失敗も死角もありません。取引は常に保護されます。</string>
<string name="swap_story_forth_subtitle_v2">スワップは信頼できるプロバイダーを通じて実行されます。秘密鍵は常にTangemウォレット内に保持されます。明確で、透明性が高く、自己管理型です。</string>
<string name="swap_story_forth_title">難攻不落の防御</string>
<string name="swap_story_forth_title_v2">主導権はあなたの手にあります</string>
<string name="swap_story_forth_title_v2">主導権はあなたの手に</string>
<string name="swap_story_second_subtitle">幅広いネットワークの中から、常に最適なプロバイダーと料金レートを選択します</string>
<string name="swap_story_second_subtitle_v2">TangemはDEXとCEXを含む複数のプロバイダーを比較し、最良レートを自動で選択します。別のプロバイダーを希望する場合は、手動で選択できます。</string>
<string name="swap_story_second_title">破格のレート</string>
@ -1860,6 +1868,7 @@
<string name="transaction_history_transaction_validator">バリデーター: %s</string>
<string name="transaction_notifications_warning_description">通知は有効になっていますが、デバイスの設定で通知を許可するまでは機能しません。</string>
<string name="transaction_notifications_warning_title">取引通知</string>
<string name="transfer_in_progress_title">送金処理中</string>
<string name="transfer_min_amount_error">最小%s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">最小取引金額は%1$sです。</string>
<string name="tron_will_be_send_token_fee_description">Tronネットワークの人気トークンの手数料は高い可能性があります。TRXをステーキングすると、取引コストを削減できる可能性があります。</string>
@ -1891,7 +1900,9 @@
<string name="user_push_notification_agreement_argument_three">受信取引の通知を受け取る</string>
<string name="user_push_notification_agreement_argument_two">新しいプロモーション情報をいち早く入手しましょう</string>
<string name="user_push_notification_agreement_argument_two_subtitle">新しい機能や限定オファーへの早期アクセス。</string>
<string name="user_push_notification_agreement_argument_two_subtitle_v2">価格変動アラート、製品ニュース、限定オファー</string>
<string name="user_push_notification_agreement_argument_two_title">機能およびニュースのアップデート</string>
<string name="user_push_notification_agreement_argument_two_title_v2">オファー・最新情報</string>
<string name="user_push_notification_agreement_header">プッシュ通知を使用しますか?</string>
<string name="user_push_notification_banner_subtitle">プッシュ通知を有効にすると、ウォレットに着金したときにアラートを受信できます。</string>
<string name="user_push_notification_banner_title">取引を見逃さない</string>

View file

@ -80,6 +80,9 @@
<string name="action_buttons_swap_not_enough_tokens_alert_title">Adicionar tokens</string>
<string name="action_buttons_you_want_to_receive">Escolha o token que deseja receber.</string>
<string name="action_buttons_you_want_to_swap">Escolha o token que deseja trocar.</string>
<string name="actionbutton_addfunds_title">Adicionar fundos</string>
<string name="actionbutton_swap_title">Trocar</string>
<string name="actionbutton_transfer_title">Transferir</string>
<string name="add_and_manage_sheet_manage_subtitle">Adicione ao seu portfólio</string>
<string name="add_and_manage_sheet_manage_title">Adicionar tokens</string>
<string name="add_and_manage_sheet_organize_subtitle">Classificação e agrupamento</string>
@ -87,6 +90,10 @@
<string name="add_custom_token_choose_network">Escolha a rede</string>
<string name="add_custom_token_title">Adicionar token personalizado</string>
<string name="add_tokens_title">Gerenciar tokens</string>
<string name="addfunds_buy_row_description">Cartão de crédito ou conta bancária</string>
<string name="addfunds_receive_row_description">Compartilhe seu endereço ou código QR.</string>
<string name="addfunds_swap_row_description">Entre seus portfólios</string>
<string name="addfunds_you_receive_title">Você recebe</string>
<string name="address_qr_code_message_format">Enviar somente %1$s (%2$s) de %3$s A rede não está vinculada a este endereço. O uso de outros tokens e redes pode resultar na perda de fundos.</string>
<string name="address_type_default">Padrão</string>
<string name="address_type_legacy">Legado</string>
@ -217,6 +224,7 @@
<string name="common_access_denied">Acesso negado</string>
<string name="common_account">Conta</string>
<string name="common_accounts">Contas</string>
<string name="common_action_failed">%s fracassado</string>
<string name="common_activate">Ativar</string>
<string name="common_add">Adicionar</string>
<string name="common_add_funds">Adicionar fundos</string>
@ -233,6 +241,8 @@
<string name="common_apply">Aplicar</string>
<string name="common_approval">Aprovação</string>
<string name="common_approve">Aprovar</string>
<string name="common_approved">Aprovado</string>
<string name="common_approving">Aprovando</string>
<string name="common_attention">Atenção</string>
<string name="common_available_networks">Redes disponíveis</string>
<string name="common_backup">Backup</string>
@ -315,6 +325,10 @@
<string name="common_hide">Esconder</string>
<string name="common_hold_to">Mantenha-se em %s</string>
<string name="common_hour">hora</string>
<plurals name="common_hours">
<item quantity="one">%d hora</item>
<item quantity="other">%d horas</item>
</plurals>
<plurals name="common_hours_time_ago">
<item quantity="one">UM</item>
<item quantity="other">OUTRO</item>
@ -365,6 +379,8 @@
<string name="common_range_with_space">%1$s — %2$s</string>
<string name="common_read_more">Leia mais</string>
<string name="common_receive">Receber</string>
<string name="common_received">Recebido</string>
<string name="common_receiving">Recebendo</string>
<string name="common_recommended">Recomendado</string>
<string name="common_reject">Rejeitar</string>
<string name="common_reload">Recarregar</string>
@ -383,6 +399,8 @@
<string name="common_send">Enviar</string>
<string name="common_send_colon">Enviar:</string>
<string name="common_send_tx_error">Falha ao enviar a transação</string>
<string name="common_sending">Enviando</string>
<string name="common_sent">Enviado</string>
<string name="common_server_unavailable">O servidor não está disponível. Tente novamente mais tarde.</string>
<string name="common_share">Compartilhar</string>
<string name="common_share_link">Compartilhar link</string>
@ -393,6 +411,7 @@
<string name="common_skip">Pular</string>
<string name="common_something_went_wrong">Algo deu errado</string>
<string name="common_stake">Stake</string>
<string name="common_staked">Estacado</string>
<string name="common_staking">Apostas</string>
<string name="common_start">Começar</string>
<string name="common_submit">Enviar</string>
@ -400,6 +419,8 @@
<string name="common_support">Suporte</string>
<string name="common_supported_networks">Redes suportadas</string>
<string name="common_swap">Trocar</string>
<string name="common_swapped">Trocado</string>
<string name="common_swapping">Troca</string>
<string name="common_tangem">Tangem</string>
<string name="common_tangem_wallet">Carteira Tangem</string>
<string name="common_tap_and_hold_hint">Toque e segure</string>
@ -418,6 +439,8 @@
<string name="common_transaction_status">Status da transação</string>
<string name="common_transactions">Transações</string>
<string name="common_transfer">Transferir</string>
<string name="common_transferred">Transferido</string>
<string name="common_try_again_later">Por favor, tente novamente mais tarde.</string>
<string name="common_unable_to_load">Não foi possível carregar os dados…</string>
<string name="common_understand">Eu entendo</string>
<string name="common_understand_continue">Entendi, continue.</string>
@ -427,10 +450,12 @@
<string name="common_unstake">Unstake</string>
<string name="common_utxo_validate_withdrawal_message_warning">Devido a %1$s limitações apenas %2$d Os UTXOs podem caber em uma única transação. Isso significa que você só pode enviar %3$s ou menos. Você precisa reduzir a quantidade.</string>
<string name="common_value_copied">Valor copiado</string>
<string name="common_voting">Votação</string>
<string name="common_wallets">Carteiras</string>
<string name="common_warning">Aviso</string>
<string name="common_week">semana</string>
<string name="common_with">com</string>
<string name="common_withdrawing">Retirada</string>
<string name="common_yes">Sim</string>
<string name="common_yield_mode">Modo de rendimento</string>
<string name="contract_address_copied_message">Endereço do contrato copiado!</string>
@ -592,6 +617,7 @@
<string name="express_provider_best_rate">Melhor tarifa</string>
<string name="express_provider_fca_warning_list">Lista de advertências da FCA</string>
<string name="express_provider_fixed_rate_is_unavailable">A tarifa fixa não está disponível.</string>
<string name="express_provider_for_swap">Fornecedor para troca</string>
<string name="express_provider_great_rate">Tarifa competitiva</string>
<string name="express_provider_in_fca_warning_list">Fornecedor na lista de advertências da FCA</string>
<string name="express_provider_max_amount">Disponível até %s</string>
@ -673,6 +699,8 @@
<string name="hw_backup_banner_description">Mantém suas criptomoedas seguras e offline. Fino como um cartão de crédito, mais seguro que um cofre de banco.</string>
<string name="hw_backup_close_description">Se fizer isso, terá que começar tudo de novo.</string>
<string name="hw_backup_google_drive_description">Recupere sua carteira existente por meio do backup do Google Drive.</string>
<string name="hw_backup_google_drive_dialog_message">Estamos trabalhando no backup do Google Drive para facilitar ainda mais a recuperação da carteira.</string>
<string name="hw_backup_google_drive_dialog_title">O backup do Google Drive estará disponível em breve.</string>
<string name="hw_backup_google_drive_title">Backup do Google Drive</string>
<string name="hw_backup_hardware_create_description">Crie uma carteira segura e transfira seus fundos para obter proteção adicional.</string>
<string name="hw_backup_hardware_create_title">Criar nova carteira</string>
@ -1043,6 +1071,7 @@
<string name="onboarding_create_wallet_options_button_options">Outras opções</string>
<string name="onboarding_create_wallet_options_message">Suas chaves serão geradas com segurança dentro do chip. Não há frase mnemônica, o que significa que ninguém pode exportá-las ou roubá-las.</string>
<string name="onboarding_create_wallet_options_title">Gere chaves de forma privada</string>
<string name="onboarding_create_wallet_term_of_conditions_text">Ao continuar, você concorda com os termos. %1$s</string>
<string name="onboarding_done_body">Seu cartão está ativado e pronto para uso.</string>
<string name="onboarding_done_header">Sucesso!</string>
<string name="onboarding_done_wallet">Sua carteira está configurada e pronta para uso!</string>
@ -1168,6 +1197,8 @@
<string name="onramp_via_provider">via %s</string>
<string name="onramp_you_will_pay_title">Você pagará</string>
<string name="organize_tokens_group">Grupo</string>
<string name="organize_tokens_menu_group_by_networks">Agrupar por redes</string>
<string name="organize_tokens_menu_sort_by_balance">Ordenar por saldo</string>
<string name="organize_tokens_sort_by_balance">Por equilíbrio</string>
<string name="organize_tokens_title">Organizar tokens</string>
<string name="organize_tokens_ungroup">Desagrupar</string>
@ -1193,6 +1224,8 @@
<string name="qr_scanner_warning_unknown_parameters_title">Parâmetros desconhecidos</string>
<string name="quick_action_buy_description">Cartão de crédito ou conta bancária</string>
<string name="quick_action_receive_description">Compartilhe seu endereço ou código QR.</string>
<string name="quick_action_sell_description">Venda criptomoedas com segurança</string>
<string name="quick_action_send_description">Enviar para outra carteira</string>
<string name="quick_action_swap_description">Entre seus portfólios</string>
<string name="quick_top_up_chip_other">Outro</string>
<string name="quick_top_up_title">Recarga rápida</string>
@ -1442,6 +1475,7 @@
<string name="staking_details_unbonding_period_info">O período que você deve aguardar após solicitar o saque de fundos do staking para que os tokens fiquem disponíveis.</string>
<string name="staking_details_warmup_period">Período de aquecimento</string>
<string name="staking_details_warmup_period_info">O período alocado para ativar a participação no staking.</string>
<string name="staking_enabled">Staking ativado</string>
<string name="staking_error_no_validators_message">Não há validadores disponíveis no momento. Tente novamente mais tarde.</string>
<string name="staking_error_no_validators_title">Staking indisponível</string>
<string name="staking_give_permission_fee_footer">A rede cobrará uma taxa de aprovação de token para verificar se você está autorizando o uso do seu token para staking.</string>
@ -1552,6 +1586,7 @@
<string name="sui_not_enough_coin_for_fee_description">Uma transação de entrada de pelo menos %1$s é necessário prosseguir</string>
<string name="sui_not_enough_coin_for_fee_title">Fundos insuficientes</string>
<string name="swap_approve_description">Ao aprovar, você permite que o contrato inteligente utilize seus tokens em transações futuras.</string>
<string name="swap_detailed_mode">Modo detalhado</string>
<string name="swap_fixed_rate">Taxa fixa</string>
<string name="swap_give_permission_fee_footer">A rede cobrará uma taxa de aprovação de token para verificar se você está autorizando o uso do seu token para a troca.</string>
<string name="swap_in_progress">Troca em andamento</string>
@ -1560,12 +1595,13 @@
<string name="swap_search_suggestion_hint">Procurando algo diferente?\n Experimente pesquisar ou explore outra criptomoeda!</string>
<string name="swap_search_tooltip_description">Pesquise qualquer token, mesmo que ele ainda não esteja na sua lista.</string>
<string name="swap_search_tooltip_title">Use a busca para encontrar o que você precisa.</string>
<string name="swap_simple_mode">Modo simples</string>
<string name="swap_story_fifth_subtitle">Sinta-se seguro com o suporte disponível 24 horas por dia, 7 dias por semana, para ajudá-lo com qualquer problema.</string>
<string name="swap_story_fifth_title">Sempre aqui</string>
<string name="swap_story_first_subtitle">Diversos provedores confiáveis em um só lugar — troque qualquer ativo facilmente em sua carteira.</string>
<string name="swap_story_first_subtitle_v2">Troque criptomoedas diretamente na Tangem\nSem transferências adicionais\nSem movimentação de fundos para corretoras</string>
<string name="swap_story_first_title">Troque conosco</string>
<string name="swap_story_first_title_v2">Troque o conteúdo da sua carteira.</string>
<string name="swap_story_first_title_v2">Troque dentro de sua carteira</string>
<string name="swap_story_forth_subtitle">Sem erros, sem perdas de posse, sem pontos cegos — sua transação está sempre protegida.</string>
<string name="swap_story_forth_subtitle_v2">As trocas são executadas por meio de provedores confiáveis. Suas chaves permanecem em sua carteira Tangem o tempo todo. Clara. Transparente. Autocustódia.</string>
<string name="swap_story_forth_title">Defesa Impenetrável</string>
@ -1577,7 +1613,7 @@
<string name="swap_story_third_subtitle">Sem complicações e intuitivo, permitindo que você troque tokens com apenas alguns toques</string>
<string name="swap_story_third_subtitle_v2">Troca entre as principais redes e milhares de tokens 0% Taxa em trocas de stablecoins</string>
<string name="swap_story_third_title">Simplesmente conveniente</string>
<string name="swap_story_third_title_v2">Mais de 90 blockchains | Mais de 16.000 ativos</string>
<string name="swap_story_third_title_v2">mais de 90 blockchains\n16.000+ ativos</string>
<string name="swap_via_provider">Trocar através do provedor</string>
<string name="swap_your_assets_title">Seus ativos</string>
<string name="swapping_alert_cex_description">O valor inclui:\n• taxa do provedor de serviços\n• taxa de rede para envio %s da central de distribuição de volta para o endereço do usuário.</string>
@ -1605,6 +1641,8 @@
<string name="swapping_token_not_available">não disponível</string>
<string name="swapping_trade_too_large_text">Liquidez insuficiente para esta transação. \n o valor ou escolha outro provedor.</string>
<string name="swapping_trade_too_large_title">Negociação em grande escala</string>
<string name="swapping_transfer_action">Transferir</string>
<string name="swapping_transfer_action_in_progress">Transferir...</string>
<string name="tangem_pay_beta_notification_subtitle">Teremos todo o prazer em receber seu feedback.</string>
<string name="tangem_pay_beta_notification_title">O Tangem Pay agora está em versão beta.</string>
<string name="tangem_pay_card_details_unable_to_rename_card_title">Não foi possível renomear o cartão.</string>
@ -1681,6 +1719,7 @@
<string name="tangempay_card_details_reissue_card">Substituir cartão</string>
<string name="tangempay_card_details_rename_card_invalid_description">Somente letras e números são permitidos.</string>
<string name="tangempay_card_details_rename_card_invalid_title">Caracteres inválidos</string>
<string name="tangempay_card_details_rename_card_placeholder">Nome do cartão</string>
<string name="tangempay_card_details_reveal_text">Revelar</string>
<string name="tangempay_card_details_show_details">Mostrar detalhes</string>
<string name="tangempay_card_details_swap_description">Troque qualquer ativo da sua carteira por um cartão.</string>
@ -1693,6 +1732,7 @@
<string name="tangempay_card_details_withdraw_error_title">Retirada indisponível agora</string>
<string name="tangempay_card_details_withdraw_in_progress_description">Você não pode iniciar uma troca ou um novo saque até que o atual seja concluído.</string>
<string name="tangempay_card_details_withdraw_in_progress_title">Retirada em andamento</string>
<string name="tangempay_card_edit_name_placeholder">Nome do cartão</string>
<string name="tangempay_card_limit_setup_amount_subtitle">Definir um limite a partir de %s</string>
<string name="tangempay_card_limit_setup_error_message">Não foi possível definir o limite. Tente novamente.</string>
<string name="tangempay_card_page_daily_limit_change">Mudar</string>
@ -1703,6 +1743,10 @@
<string name="tangempay_card_page_daily_limit_success_title">O limite diário está definido.</string>
<string name="tangempay_card_page_daily_limit_title">Limite diário</string>
<string name="tangempay_card_page_settings_title">Configurações do cartão</string>
<plurals name="tangempay_cards_count">
<item quantity="one">%d cartão</item>
<item quantity="other">%d cartões</item>
</plurals>
<string name="tangempay_change_pin_code">Alterar código PIN</string>
<string name="tangempay_come_back_if_forget_pin">Volte ao aplicativo se você se esquecer.</string>
<string name="tangempay_daily_limit_hint" formatted="false">Defina um limite a partir de %s para %s</string>
@ -1717,8 +1761,15 @@
<string name="tangempay_get_banner_description">Obtenha seu cartão virtual Visa Tangem grátis.</string>
<string name="tangempay_get_tangem_pay">Obtenha o Tangem Pay</string>
<string name="tangempay_go_to_support">Acesse o Suporte</string>
<string name="tangempay_issue_additional_card_description">Isso gera um novo conjunto de detalhes do cartão.</string>
<string name="tangempay_issue_additional_card_fee_label">Taxa de emissão</string>
<string name="tangempay_issue_additional_card_insufficient_funds_subtitle">Deposite USDC na conta de pagamento para cobrir a taxa de emissão.</string>
<string name="tangempay_issue_additional_card_insufficient_funds_title">Não foi possível cobrir a taxa.</string>
<string name="tangempay_issue_additional_card_title">Emitir um cartão adicional?</string>
<string name="tangempay_issue_card">Emitir cartão</string>
<string name="tangempay_issue_card_notification_description">Geralmente leva até 15 minutos.</string>
<string name="tangempay_issue_card_notification_title">Configurando seu cartão Tangem</string>
<string name="tangempay_issuing_new_digital_card_title">Emissão de um novo cartão digital</string>
<string name="tangempay_issuing_your_card">Emissão do seu cartão</string>
<string name="tangempay_issuing_your_card_description">O cartão geralmente é emitido automaticamente em até 5 minutos. Em casos raros, se for necessária uma análise manual, o processo pode levar até 48 horas.</string>
<string name="tangempay_kyc_card_ready_notification_title">Tangem Pay</string>
@ -1735,6 +1786,8 @@
<string name="tangempay_kyc_rejected_button_text">Ocultar bloco KYC</string>
<string name="tangempay_kyc_rejected_description">Desculpe, não foi possível verificar.</string>
<string name="tangempay_kyc_rejected_description_span">Seu perfil.</string>
<string name="tangempay_maximum_cards_issued_description">Você pode ter até 3 cartões. Exclua um para adicionar um novo.</string>
<string name="tangempay_maximum_cards_issued_title">Número máximo de cartões emitidos</string>
<string name="tangempay_onboarding_banner_description">Obtenha seu cartão virtual Visa Tangem grátis.</string>
<string name="tangempay_onboarding_banner_title">Use USDC para pagamentos do dia a dia.</string>
<string name="tangempay_onboarding_get_card_button_text">Obter cartão</string>
@ -1744,6 +1797,8 @@
<string name="tangempay_onboarding_purchases_title">Pague exatamente o que você vê.</string>
<string name="tangempay_onboarding_security_description">Uma conta de pagamento separada será criada sem divulgar seus endereços e bens.</string>
<string name="tangempay_onboarding_security_title">Privacidade incomparável</string>
<string name="tangempay_onboarding_setup_wallet_description">E vincule um cartão de pagamento a ele.</string>
<string name="tangempay_onboarding_setup_wallet_title">Vamos configurar uma carteira.</string>
<string name="tangempay_onboarding_title">Obtenha seu cartão Tangem Pay gratuito em minutos.</string>
<string name="tangempay_pay_support">Suporte de Pay</string>
<string name="tangempay_payment_account">Conta de pagamento</string>
@ -1764,6 +1819,7 @@
<string name="tangempay_service_unreachable_try_later">Não foi possível exibir os detalhes. No entanto, os pagamentos com cartão ainda estão funcionando.</string>
<string name="tangempay_set_pin_code">Defina o código PIN.</string>
<string name="tangempay_status_deactivated">Cartão desativado</string>
<string name="tangempay_status_replacing">Substituindo seu cartão</string>
<string name="tangempay_sync_needed">Sessão expirada</string>
<string name="tangempay_sync_needed_restore_access">Restaurar acesso</string>
<string name="tangempay_tangem_visa_card">Use USDC para pagamentos do dia a dia.</string>
@ -1819,18 +1875,28 @@
<string name="tokens_list_unavailable_to_sell_header">Não disponível para venda</string>
<string name="tokens_list_unavailable_to_swap_header">Não disponível para troca a partir de %s</string>
<string name="tokens_list_unavailable_to_swap_source_header">Indisponível para troca</string>
<string name="transaction_history_claiming_reward">Reivindicar recompensa</string>
<string name="transaction_history_contract_address">contrato: %s</string>
<string name="transaction_history_disabling_yield_mode">Desativar o modo Yield</string>
<string name="transaction_history_earned_from_stake">Ganhos com participação</string>
<string name="transaction_history_empty_transactions">Você ainda não possui nenhuma transação.</string>
<string name="transaction_history_error_failed_to_load">Falha ao carregar o histórico de transações. Clique no botão \n para atualizar as informações.</string>
<string name="transaction_history_from_inline_address">de: %%image%% %s</string>
<string name="transaction_history_multiple_addresses">Vários endereços</string>
<string name="transaction_history_not_supported_description">O histórico de transações ainda não é suportado nesta blockchain. Mas não se preocupe, estamos trabalhando nisso! Enquanto isso, você pode conferir no explorador.</string>
<string name="transaction_history_operation">Operação</string>
<string name="transaction_history_pending">Pendente</string>
<string name="transaction_history_rewards_restaked">Recompensas redefinidas</string>
<string name="transaction_history_rewards_restaking">Recompensas por reconfiguração</string>
<string name="transaction_history_staking_reward">Recompensa de staking</string>
<string name="transaction_history_to_inline_address">para: %%image%% %s</string>
<string name="transaction_history_transaction_for_address">para: %s</string>
<string name="transaction_history_transaction_from_address">de: %s</string>
<string name="transaction_history_transaction_to_address">para: %s</string>
<string name="transaction_history_transaction_validator">validador: %s</string>
<string name="transaction_notifications_warning_description">As notificações estão ativadas, mas não funcionarão até que você as permita nas configurações do seu dispositivo.</string>
<string name="transaction_notifications_warning_title">Notificações de transação</string>
<string name="transfer_in_progress_title">Transferência em andamento</string>
<string name="transfer_min_amount_error">Mínimo %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">O valor mínimo da transação é %1$s.</string>
<string name="tron_will_be_send_token_fee_description">As taxas da rede Tron para tokens populares podem ser mais altas. Fazer staking de TRX pode ajudar a reduzir os custos de transação.</string>
@ -1862,7 +1928,9 @@
<string name="user_push_notification_agreement_argument_three">Receba notificações de transações recebidas.</string>
<string name="user_push_notification_agreement_argument_two">Seja o primeiro a saber sobre novas promoções.</string>
<string name="user_push_notification_agreement_argument_two_subtitle">Acesso antecipado a novas funcionalidades e ofertas exclusivas.</string>
<string name="user_push_notification_agreement_argument_two_subtitle_v2">Alertas de alteração de preços, novidades sobre produtos e ofertas exclusivas.</string>
<string name="user_push_notification_agreement_argument_two_title">Atualizações de notícias e recursos</string>
<string name="user_push_notification_agreement_argument_two_title_v2">Ofertas e atualizações</string>
<string name="user_push_notification_agreement_header">Você gostaria de usar notificações push?</string>
<string name="user_push_notification_banner_subtitle">Ative as notificações push para receber alertas quando os fundos chegarem à sua carteira.</string>
<string name="user_push_notification_banner_title">Não perca nenhuma transação.</string>
@ -2104,6 +2172,10 @@
<item quantity="one">Use seu Cartão ou Anel para obter o endereço de uma rede %d.</item>
<item quantity="other">Use seu Cartão ou Anel para obter endereços de redes %d.</item>
</plurals>
<plurals name="warning_missing_derivation_no_nfc_message">
<item quantity="one">Sincronizar endereços para obter um endereço de %d rede</item>
<item quantity="other">Sincronizar endereços para obter endereços para %d rede</item>
</plurals>
<string name="warning_missing_derivation_title">Alguns endereços estão faltando.</string>
<string name="warning_network_unreachable_message">A rede está inacessível no momento. Tente novamente mais tarde.</string>
<string name="warning_network_unreachable_title">A rede está inacessível.</string>

View file

@ -80,6 +80,9 @@
<string name="action_buttons_swap_not_enough_tokens_alert_title">Добавьте токены</string>
<string name="action_buttons_you_want_to_receive">Выберите токен для получения</string>
<string name="action_buttons_you_want_to_swap">Выберите токен для обмена</string>
<string name="actionbutton_addfunds_title">Пополнить</string>
<string name="actionbutton_swap_title">Обмен</string>
<string name="actionbutton_transfer_title">Перевод</string>
<string name="add_and_manage_sheet_manage_subtitle">Добавить в портфель</string>
<string name="add_and_manage_sheet_manage_title">Добавить токен</string>
<string name="add_and_manage_sheet_organize_subtitle">Сортировка и группировка</string>
@ -87,6 +90,9 @@
<string name="add_custom_token_choose_network">Выберите сеть</string>
<string name="add_custom_token_title">Добавить токен</string>
<string name="add_tokens_title">Валюты</string>
<string name="addfunds_receive_row_description">Поделитесь своим адресом или QR-кодом.</string>
<string name="addfunds_swap_row_description">Между вашими портфелями</string>
<string name="addfunds_you_receive_title">Вы получите</string>
<string name="address_qr_code_message_format">Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств.</string>
<string name="address_type_default">По умолчанию</string>
<string name="address_type_legacy">Устаревший</string>
@ -512,7 +518,7 @@
<string name="details_row_title_signed_hashes">Подписано</string>
<string name="details_send_feedback">Отправить отзыв</string>
<string name="details_title">Подробности</string>
<string name="details_wallets_section_description">У вас может быть только один мобильный кошелек. Вы можете апгрейдить его до аппаратного кошелька Tangem или добавить новый аппаратный кошелек.</string>
<string name="details_wallets_section_description">Мобильный кошелек может быть только один. Перенесите его на аппаратный кошелек Tangem или добавьте новый аппаратный кошелек.</string>
<string name="disclaimer_error_loading">Проверьте подключение с интернетом или переключитесь на другую сеть</string>
<string name="disclaimer_title">Условия использования</string>
<string name="domain_receive_assets_default_address">Основной адрес</string>
@ -1046,6 +1052,13 @@
<string name="onboarding_activation_error_message">Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам</string>
<string name="onboarding_activation_error_title">Ошибка активации</string>
<string name="onboarding_add_tokens">Добавление токенов</string>
<plurals name="onboarding_address_sync_description">
<item quantity="one">В вашем кошельке %d токен. Синхронизируйте адреса, чтобы продолжить.</item>
<item quantity="few">В вашем кошельке %d токена. Синхронизируйте адреса, чтобы продолжить.</item>
<item quantity="many">В вашем кошельке %d токенов. Синхронизируйте адреса, чтобы продолжить.</item>
<item quantity="other">В вашем кошельке %d токенов. Синхронизируйте адреса, чтобы продолжить.</item>
</plurals>
<string name="onboarding_address_sync_title">Синхронизируйте ваш кошелек</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">Вы добавили одну резервную карту или кольцо. После того, как процесс будет завершен, Вы больше не сможете добавить еще. Если у Вас есть еще одна карта или кольцо, добавьте ее в резервную копию. Хотите продолжить?</string>
<string name="onboarding_backup_exit_warning">Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.</string>
<string name="onboarding_bottom_sheet_passphrase_description">Парольная фраза — это дополнительная функция безопасности, которая добавляет слово или фразу к вашей фразе восстановления, создавая новый набор адресов кошелька для дополнительной защиты.</string>
@ -1067,6 +1080,7 @@
<string name="onboarding_create_wallet_options_button_options">Другие опции</string>
<string name="onboarding_create_wallet_options_message">Ваши ключи будут надежно сгенерированы внутри чипа. Никакой seed-фразы, а это значит, что никто не может экспортировать или украсть ее.</string>
<string name="onboarding_create_wallet_options_title">Cоздавайте ключи приватно</string>
<string name="onboarding_create_wallet_term_of_conditions_text">Продолжая, вы соглашаетесь с \n%s</string>
<string name="onboarding_done_body">Ваша карта активирована и готова к использованию</string>
<string name="onboarding_done_header">Успешно!</string>
<string name="onboarding_done_wallet">Ваш кошелек настроен и готов к использованию!</string>
@ -1188,6 +1202,8 @@
<string name="onramp_via_provider">через %s</string>
<string name="onramp_you_will_pay_title">Вы заплатите</string>
<string name="organize_tokens_group">Группы</string>
<string name="organize_tokens_menu_group_by_networks">Сгруппировать по сети</string>
<string name="organize_tokens_menu_sort_by_balance">Сортировать по балансу</string>
<string name="organize_tokens_sort_by_balance">По балансу</string>
<string name="organize_tokens_title">Упорядочить токены</string>
<string name="organize_tokens_ungroup">Список</string>
@ -1581,13 +1597,21 @@
<string name="swap_story_fifth_subtitle">Чувствуйте уверенность с круглосуточной поддержкой, готовой помочь в любой ситуации!</string>
<string name="swap_story_fifth_title">Круглосуточная поддержка</string>
<string name="swap_story_first_subtitle">Надежные провайдеры в одном месте — обменивайте любые активы легко и быстро прямо в своем кошельке!</string>
<string name="swap_story_first_subtitle_v2">Обменивайте крипту прямо в Tangem\nНикаких лишних переводов\nНе нужно отправлять средства на биржи</string>
<string name="swap_story_first_title">Обменивайте с нами</string>
<string name="swap_story_first_title_v2">Обмен прямо в кошельке</string>
<string name="swap_story_forth_subtitle">Высший уровень безопасности и проверенные партнёры защищают каждую транзакцию</string>
<string name="swap_story_forth_subtitle_v2">Обмен осуществляется через надежных провайдеров. Ваши ключи всегда остаются в кошельке Tangem. Понятно. Прозрачно. Некастодиально.</string>
<string name="swap_story_forth_title">Полная защита</string>
<string name="swap_story_forth_title_v2">Всё Под Вашим Контролем</string>
<string name="swap_story_second_subtitle">Автоматически находим самые выгодные предложения среди проверенных провайдеров</string>
<string name="swap_story_second_subtitle_v2">Tangem сравнивает несколько провайдеров, как DEX, так и CEX. Лучший курс выбирается автоматически. Предпочитаете другого провайдера? Вы можете выбрать его вручную.</string>
<string name="swap_story_second_title">Лучшие курсы</string>
<string name="swap_story_second_title_v2">Лучший Доступный Курс</string>
<string name="swap_story_third_subtitle">Интуитивный обмен в пару касаний — без сложностей и ожидания</string>
<string name="swap_story_third_subtitle_v2">Обменивайте тысячи токенов между любыми сетями. Комиссия 0% на обмен стейблкоинов</string>
<string name="swap_story_third_title">Проще простого</string>
<string name="swap_story_third_title_v2">90+ Блокчейнов\n16 000+ Активов</string>
<string name="swap_via_provider">Обмен через провайдера</string>
<string name="swap_your_assets_title">Ваши средства</string>
<string name="swapping_alert_cex_description">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя.</string>
@ -1833,6 +1857,7 @@
<string name="transaction_history_transaction_validator">валидатор: %s</string>
<string name="transaction_notifications_warning_description">Уведомления включены, но не будут работать, пока вы не разрешите их в настройках устройства.</string>
<string name="transaction_notifications_warning_title">Уведомления о транзакциях</string>
<string name="transfer_in_progress_title">Перевод в процессе</string>
<string name="transfer_min_amount_error">Минимум %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Минимальная сумма транзакции равна %1$s.</string>
<string name="tron_will_be_send_token_fee_description">Комиссии сети Tron для популярных токенов могут быть выше. Стейкинг TRX может помочь снизить расходы на транзакции.</string>
@ -1975,7 +2000,7 @@
<string name="wallet_settings_set_access_code_title">Установить Код Доступа</string>
<string name="wallet_settings_title">Настройки кошелька</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Используйте %s или отсканируйте карту/кольцо, чтобы разблокировать доступ к вашему кошельку</string>
<string name="warning_access_denied_message">Воспользуйтесь %s или отсканируйте карту/кольцо, чтобы разблокировать доступ к вашему кошельку</string>
<string name="warning_approval_in_progress_message">Процесс выдачи разрешения уже в работе и скоро будет завершен</string>
<string name="warning_approval_in_progress_title">Выдача разрешения</string>
<string name="warning_backup_errors_message">Похоже, что процесс активации карт или кольца не был завершен корректно. Это могло быть вызвано проблемой взаимодействия с модулем NFC либо некорректным прикладыванием карты или кольца к телефону. Пожалуйста, обратитесь в нашу службу поддержки для уточнения деталей.</string>
@ -2049,6 +2074,12 @@
<item quantity="many">Используйте вашу карту или кольцо, чтобы получить адреса для %d сетей</item>
<item quantity="other">Используйте вашу карту или кольцо, чтобы получить адреса для %d сетей</item>
</plurals>
<plurals name="warning_missing_derivation_no_nfc_message">
<item quantity="one">Синхронизируйте адреса, чтобы получить адрес для %d сети</item>
<item quantity="few">Синхронизируйте адреса, чтобы получить адреса для %d сетей</item>
<item quantity="many">Синхронизируйте адреса, чтобы получить адреса для %d сетей</item>
<item quantity="other">Синхронизируйте адреса, чтобы получить адреса для %d сетей</item>
</plurals>
<string name="warning_missing_derivation_title">Некоторые адреса отсутствуют</string>
<string name="warning_network_unreachable_message">В данный момент сеть недоступна. Пожалуйста, попробуйте позже.</string>
<string name="warning_network_unreachable_title">Сеть недоступна</string>

View file

@ -719,6 +719,9 @@
<string name="initial_message_scan_header">Прикладіть, щоб відсканувати</string>
<string name="initial_message_sign_header">Прикладіть, щоб підписати</string>
<string name="initial_message_tap_header">Прикладіть картку або кільце</string>
<string name="initial_wallet_sync_banner_description">Ваш гаманець синхронізовано й готово до використання.\nНе бачите деякі токени?</string>
<string name="initial_wallet_sync_banner_title">Гаманець успішно імпортовано</string>
<string name="initial_wallet_sync_restore_progress">Відновлення %d%%</string>
<string name="key_invalidated_warning_description">Ви оновили дані біометрії, відскануйте свою картку або кільце для входу</string>
<string name="koinos_insufficient_balance_to_send_koin_description">Ваш баланс повинен перевищувати суму комісії для здійснення переказу</string>
<string name="koinos_insufficient_balance_to_send_koin_title">Недостатньо балансу</string>
@ -981,6 +984,13 @@
<string name="onboarding_activation_error_message">Необхідно повторити операцію, при цьому картка буде скинута до заводських налаштувань.</string>
<string name="onboarding_activation_error_title">Помилка активації</string>
<string name="onboarding_add_tokens">Додати токени</string>
<plurals name="onboarding_address_sync_description">
<item quantity="one">У вашому гаманці %d токен. Синхронізуйте адреси, щоб продовжити.</item>
<item quantity="few">У вашому гаманці %d токени. Синхронізуйте адреси, щоб продовжити.</item>
<item quantity="many">У вашому гаманці %d токенів. Синхронізуйте адреси, щоб продовжити.</item>
<item quantity="other">У вашому гаманці %d токенів. Синхронізуйте адреси, щоб продовжити.</item>
</plurals>
<string name="onboarding_address_sync_title">Синхронізуйте свій гаманець</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">Ви додали одну резервну картку або кільце. Після завершення процесу резервного копіювання ви не зможете додати їх більше. Якщо у вас є ще одна картка або кільце, додайте її до резервної копії. Бажаєте продовжити?</string>
<string name="onboarding_backup_exit_warning">Процес резервного копіювання частково завершено. Ви не можете вийти з нього зараз.</string>
<string name="onboarding_bottom_sheet_passphrase_description">Парольна фраза це розширена функція безпеки, яку використовують криптогаманці. Вона додає додаткове слово або фразу на ваш вибір до вже існуючої seed - фрази, щоб розблокувати абсолютно новий набір адрес.</string>
@ -1002,6 +1012,7 @@
<string name="onboarding_create_wallet_options_button_options">Інші опції</string>
<string name="onboarding_create_wallet_options_message">Ваші ключі будуть надійно згенеровані всередині чіпу. Ніякої seed-фрази, а це означає, що ніхто не зможе її експортувати або вкрасти.</string>
<string name="onboarding_create_wallet_options_title">Генеруйте ключі приватно</string>
<string name="onboarding_create_wallet_term_of_conditions_text">Продовжуючи, ви погоджуєтеся з \n%s</string>
<string name="onboarding_done_body">Ваша картка активована та готова до використання</string>
<string name="onboarding_done_header">Успішно!</string>
<string name="onboarding_done_wallet">Ваш гаманець налаштований і готовий до використання!</string>
@ -1508,13 +1519,21 @@
<string name="swap_story_fifth_subtitle">Надійна підтримка 24/7, щоб ваші фінансові операції були швидкими та надійними!</string>
<string name="swap_story_fifth_title">Цілодобова підтримка</string>
<string name="swap_story_first_subtitle">Надійні провайдери дозволяють легко обмінювати активи, забезпечуючи повну безпеку у вашому гаманці</string>
<string name="swap_story_first_subtitle_v2">Обмінюйте крипту прямо в Tangem\nНіяких зайвих переказів\nНе потрібно відправляти кошти на біржі</string>
<string name="swap_story_first_title">Обмінюйте з нами</string>
<string name="swap_story_first_title_v2">Обмін прямо в гаманці</string>
<string name="swap_story_forth_subtitle">Найвищий рівень безпеки та перевірені провайдери гарантують захист ваших активів при кожному обміні.</string>
<string name="swap_story_forth_subtitle_v2">Обмін здійснюється через надійних провайдерів. Ваші ключі завжди залишаються в гаманці Tangem. Зрозуміло. Прозоро. Некастодіально.</string>
<string name="swap_story_forth_title">Безпечніше, ніж будь-коли</string>
<string name="swap_story_forth_title_v2">Усе Під Вашим Контролем</string>
<string name="swap_story_second_subtitle">Отримуйте максимальну вигоду з кращими курсами від надійних провайдерів</string>
<string name="swap_story_second_subtitle_v2">Tangem порівнює кілька провайдерів, як DEX, так і CEX. Найкращий курс обирається автоматично. Віддаєте перевагу іншому провайдеру? Ви можете обрати його вручну.</string>
<string name="swap_story_second_title">Найкращі тарифи</string>
<string name="swap_story_second_title_v2">Найкращий Доступний Курс</string>
<string name="swap_story_third_subtitle">Просто та зручно — обмін токенів в декілька дотиків</string>
<string name="swap_story_third_subtitle_v2">Обмінюйте тисячі токенів між будь-якими мережами. Комісія 0% на обмін стейблкоїнів</string>
<string name="swap_story_third_title">Легше, ніж будь-коли</string>
<string name="swap_story_third_title_v2">90+ Блокчейнів\n16 000+ Активів</string>
<string name="swap_via_provider">Обмін через провайдера</string>
<string name="swap_your_assets_title">Ваші активи</string>
<string name="swapping_alert_cex_description">Сума включає: \n• комісію постачальника послуг\n• комісію мережі за відправлення %s з біржі назад на адресу користувача.</string>
@ -1979,6 +1998,12 @@
<item quantity="many">Використовуйте вашу картку або кільце, щоб отримати адреси для %d мереж</item>
<item quantity="other">Використовуйте вашу картку або кільце, щоб отримати адреси для %d мереж</item>
</plurals>
<plurals name="warning_missing_derivation_no_nfc_message">
<item quantity="one">Синхронізуйте адреси, щоб отримати адресу для %d мережі</item>
<item quantity="few">Синхронізуйте адреси, щоб отримати адреси для %d мереж</item>
<item quantity="many">Синхронізуйте адреси, щоб отримати адреси для %d мереж</item>
<item quantity="other">Синхронізуйте адреси, щоб отримати адреси для %d мереж</item>
</plurals>
<string name="warning_missing_derivation_title">Деякі адреси відсутні</string>
<string name="warning_network_unreachable_message">Мережа наразі недоступна. Будь ласка, спробуйте пізніше.</string>
<string name="warning_network_unreachable_title">Мережа недоступна</string>

View file

@ -80,6 +80,9 @@
<string name="action_buttons_swap_not_enough_tokens_alert_title">添加代币</string>
<string name="action_buttons_you_want_to_receive">选择您想要接收的代币</string>
<string name="action_buttons_you_want_to_swap">选择您要兑换的代币</string>
<string name="actionbutton_addfunds_title">增加资金</string>
<string name="actionbutton_swap_title">兑换</string>
<string name="actionbutton_transfer_title">转账</string>
<string name="add_and_manage_sheet_manage_subtitle">添加到您的投资组合</string>
<string name="add_and_manage_sheet_manage_title">添加代币</string>
<string name="add_and_manage_sheet_organize_subtitle">排序和分组</string>
@ -87,6 +90,10 @@
<string name="add_custom_token_choose_network">选择网络</string>
<string name="add_custom_token_title">添加自定义代币</string>
<string name="add_tokens_title">管理代币</string>
<string name="addfunds_buy_row_description">信用卡或银行账户</string>
<string name="addfunds_receive_row_description">分享您的地址或二维码</string>
<string name="addfunds_swap_row_description">在您的投资组合之间</string>
<string name="addfunds_you_receive_title">您将收到</string>
<string name="address_qr_code_message_format">只能从 %3$s 网络发送 %1$s (%2$s) 到此地址。使用其他代币和网络可能会导致资金损失。</string>
<string name="address_type_default">默认</string>
<string name="address_type_legacy">传统</string>
@ -422,6 +429,7 @@
<string name="common_transactions">交易</string>
<string name="common_transfer">转让</string>
<string name="common_transferred">已转账</string>
<string name="common_try_again_later">请稍后再试。</string>
<string name="common_unable_to_load">无法加载数据……</string>
<string name="common_understand">我明白</string>
<string name="common_understand_continue">我明白,请继续</string>
@ -1045,6 +1053,7 @@
<string name="onboarding_create_wallet_options_button_options">其他选项</string>
<string name="onboarding_create_wallet_options_message">您的密钥将在芯片内部安全生成,没有助记词,这意味着任何人都无法导出或窃取它。</string>
<string name="onboarding_create_wallet_options_title">私下生成密钥</string>
<string name="onboarding_create_wallet_term_of_conditions_text">如继续,即表示您同意</string>
<string name="onboarding_done_body">您的卡已激活,可以使用了。</string>
<string name="onboarding_done_header">成功!</string>
<string name="onboarding_done_wallet">您的钱包已设置完毕,可以使用了!</string>
@ -1166,6 +1175,8 @@
<string name="onramp_via_provider">通过 %s</string>
<string name="onramp_you_will_pay_title">您将支付</string>
<string name="organize_tokens_group">分组</string>
<string name="organize_tokens_menu_group_by_networks">按网络分组</string>
<string name="organize_tokens_menu_sort_by_balance">按余额排序</string>
<string name="organize_tokens_sort_by_balance">按余额</string>
<string name="organize_tokens_title">整理代币</string>
<string name="organize_tokens_ungroup">取消分组</string>
@ -1439,6 +1450,7 @@
<string name="staking_details_unbonding_period_info">从质押账户提取资金后,您必须等待一段时间才能获得代币。</string>
<string name="staking_details_warmup_period">热身期</string>
<string name="staking_details_warmup_period_info">激活参与质押的指定时间。</string>
<string name="staking_enabled">已启用质押</string>
<string name="staking_error_no_validators_message">目前没有可用的验证节点。请稍后再试。</string>
<string name="staking_error_no_validators_title">质押功能不可用</string>
<string name="staking_give_permission_fee_footer">网络将收取代币批准费,以验证您是否授权使用您的代币进行质押。</string>
@ -1682,6 +1694,7 @@
<string name="tangempay_card_details_reissue_card">重新发行卡片</string>
<string name="tangempay_card_details_rename_card_invalid_description">只允许输入字母和数字。</string>
<string name="tangempay_card_details_rename_card_invalid_title">无效字符</string>
<string name="tangempay_card_details_rename_card_placeholder">卡片名称</string>
<string name="tangempay_card_details_reveal_text">显示</string>
<string name="tangempay_card_details_show_details">显示详情</string>
<string name="tangempay_card_details_swap_description">将您投资组合中的任何资产互换到卡片</string>
@ -1705,6 +1718,9 @@
<string name="tangempay_card_page_daily_limit_success_title">每日限额已设定</string>
<string name="tangempay_card_page_daily_limit_title">每日限额</string>
<string name="tangempay_card_page_settings_title">卡片设置</string>
<plurals name="tangempay_cards_count">
<item quantity="other">%d卡片</item>
</plurals>
<string name="tangempay_change_pin_code">更改PIN码</string>
<string name="tangempay_come_back_if_forget_pin">如果忘记了,请返回应用程序。</string>
<string name="tangempay_daily_limit_hint" formatted="false">设置限额从 %s 到 %s</string>
@ -1719,8 +1735,15 @@
<string name="tangempay_get_banner_description">免费领取您的 Tangem Visa 虚拟卡</string>
<string name="tangempay_get_tangem_pay">获取 Tangem Pay</string>
<string name="tangempay_go_to_support">前往支持页面</string>
<string name="tangempay_issue_additional_card_description">它会生成一组新的卡片详细信息。</string>
<string name="tangempay_issue_additional_card_fee_label">发行费</string>
<string name="tangempay_issue_additional_card_insufficient_funds_subtitle">将USDC存入支付账户以支付发行费用</string>
<string name="tangempay_issue_additional_card_insufficient_funds_title">无法支付费用</string>
<string name="tangempay_issue_additional_card_title">是否再发一张卡?</string>
<string name="tangempay_issue_card">发卡</string>
<string name="tangempay_issue_card_notification_description">通常需要最多15分钟</string>
<string name="tangempay_issue_card_notification_title">设置您的 Tangem 卡</string>
<string name="tangempay_issuing_new_digital_card_title">发行新的数字卡</string>
<string name="tangempay_issuing_your_card">正在发放您的卡片</string>
<string name="tangempay_issuing_your_card_description">该卡通常会在 5 分钟内自动发放。极少数情况下,如果需要人工审核,则可能需要长达 48 小时。</string>
<string name="tangempay_kyc_card_ready_notification_title">Tangem Pay</string>
@ -1737,6 +1760,8 @@
<string name="tangempay_kyc_rejected_button_text">隐藏 KYC 块</string>
<string name="tangempay_kyc_rejected_description">抱歉,我们无法验证</string>
<string name="tangempay_kyc_rejected_description_span">您的个人资料。</string>
<string name="tangempay_maximum_cards_issued_description">您最多可以添加 3 张卡片。删除一张即可添加新卡片。</string>
<string name="tangempay_maximum_cards_issued_title">最大发卡量</string>
<string name="tangempay_onboarding_banner_description">免费领取您的 Tangem Visa 虚拟卡</string>
<string name="tangempay_onboarding_banner_title">使用 USDC 进行日常支付</string>
<string name="tangempay_onboarding_get_card_button_text">获取卡片</string>
@ -1768,6 +1793,7 @@
<string name="tangempay_service_unreachable_try_later">无法显示详细信息。但刷卡支付功能仍然可用。</string>
<string name="tangempay_set_pin_code">设置 PIN 码</string>
<string name="tangempay_status_deactivated">卡片已停用</string>
<string name="tangempay_status_replacing">更换您的卡片</string>
<string name="tangempay_sync_needed">会话已过期</string>
<string name="tangempay_sync_needed_restore_access">恢复访问权限</string>
<string name="tangempay_tangem_visa_card">使用 USDC 进行日常支付</string>
@ -1829,7 +1855,7 @@
<string name="transaction_history_earned_from_stake">质押收益</string>
<string name="transaction_history_empty_transactions">您目前还没有任何交易记录。</string>
<string name="transaction_history_error_failed_to_load">加载交易历史记录失败。\n点击刷新按钮更新信息。</string>
<string name="transaction_history_from_inline_address">%image%%s</string>
<string name="transaction_history_from_inline_address">%%image%%%s</string>
<string name="transaction_history_multiple_addresses">多个地址</string>
<string name="transaction_history_not_supported_description">本区块链目前不支持交易历史记录。不过不用担心,我们正在努力!在此期间,您可以在资源管理器中查看。</string>
<string name="transaction_history_operation">操作</string>
@ -1837,13 +1863,14 @@
<string name="transaction_history_rewards_restaked">奖励已再质押</string>
<string name="transaction_history_rewards_restaking">奖励再质押</string>
<string name="transaction_history_staking_reward">质押奖励</string>
<string name="transaction_history_to_inline_address">%image%%s</string>
<string name="transaction_history_to_inline_address">%%image%%%s</string>
<string name="transaction_history_transaction_for_address">为 %s</string>
<string name="transaction_history_transaction_from_address">来自 %s</string>
<string name="transaction_history_transaction_to_address">到: %s</string>
<string name="transaction_history_transaction_validator">验证节点: %s</string>
<string name="transaction_notifications_warning_description">通知功能已启用,但需要您在设备设置中允许通知才能生效。</string>
<string name="transaction_notifications_warning_title">交易通知</string>
<string name="transfer_in_progress_title">转账中</string>
<string name="transfer_min_amount_error">最少 %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">最低交易金额为 %1$s。</string>
<string name="tron_will_be_send_token_fee_description">热门代币在波场网络上的交易费用可能较高。质押 TRX 或许有助于降低交易成本。</string>
@ -1875,7 +1902,9 @@
<string name="user_push_notification_agreement_argument_three">获取新交易通知</string>
<string name="user_push_notification_agreement_argument_two">抢先了解最新促销活动</string>
<string name="user_push_notification_agreement_argument_two_subtitle">抢先体验最新功能和专属优惠。</string>
<string name="user_push_notification_agreement_argument_two_subtitle_v2">价格变动提醒、产品资讯和独家优惠</string>
<string name="user_push_notification_agreement_argument_two_title">专题报道和新闻更新</string>
<string name="user_push_notification_agreement_argument_two_title_v2">优惠与更新</string>
<string name="user_push_notification_agreement_header">您想使用推送通知吗?</string>
<string name="user_push_notification_banner_subtitle">启用推送通知,即可在资金到账时收到提醒。</string>
<string name="user_push_notification_banner_title">不要错过任何一笔交易</string>
@ -2116,7 +2145,7 @@
<item quantity="other">使用您的卡片或指环获取%d网络地址</item>
</plurals>
<plurals name="warning_missing_derivation_no_nfc_message">
<item quantity="other">Adressen synchronisieren, um eine Adresse für das Netzwerk %d zu erhalten</item>
<item quantity="other">同步地址以获取%d网络地址</item>
</plurals>
<string name="warning_missing_derivation_title">部分地址缺失</string>
<string name="warning_network_unreachable_message">目前网络无法连接,请稍后再试。</string>

View file

@ -80,6 +80,9 @@
<string name="action_buttons_swap_not_enough_tokens_alert_title">Add tokens</string>
<string name="action_buttons_you_want_to_receive">Choose the token you want to receive</string>
<string name="action_buttons_you_want_to_swap">Choose the token you want to swap</string>
<string name="actionbutton_addfunds_title">Add funds</string>
<string name="actionbutton_swap_title">Swap</string>
<string name="actionbutton_transfer_title">Transfer</string>
<string name="add_and_manage_sheet_manage_subtitle">Add to your portfolio</string>
<string name="add_and_manage_sheet_manage_title">Add tokens</string>
<string name="add_and_manage_sheet_organize_subtitle">Sorting and grouping</string>
@ -87,6 +90,10 @@
<string name="add_custom_token_choose_network">Choose network</string>
<string name="add_custom_token_title">Add custom token</string>
<string name="add_tokens_title">Manage tokens</string>
<string name="addfunds_buy_row_description">Credit card or bank account</string>
<string name="addfunds_receive_row_description">Share your address or QR-code</string>
<string name="addfunds_swap_row_description">Between your portfolios</string>
<string name="addfunds_you_receive_title">You receive</string>
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
<string name="address_type_default">Default</string>
<string name="address_type_legacy">Legacy</string>
@ -1065,6 +1072,7 @@
<string name="onboarding_create_wallet_options_button_options">Other options</string>
<string name="onboarding_create_wallet_options_message">Your keys will be securely generated inside the chip. There is no seed phrase, which means nobody can export or steal it.</string>
<string name="onboarding_create_wallet_options_title">Generate keys privately</string>
<string name="onboarding_create_wallet_term_of_conditions_text">By continuing, you agree to the \n%s</string>
<string name="onboarding_done_body">Your card is activated and ready to be used</string>
<string name="onboarding_done_header">Success!</string>
<string name="onboarding_done_wallet">Your wallet is set up and ready to use!</string>
@ -1154,12 +1162,16 @@
<string name="onramp_legal_text">Service is provided by an external provider.\nTangem is not responsible.</string>
<string name="onramp_max_amount_restriction">The purchase amount should be no more than %s</string>
<string name="onramp_min_amount_restriction">The amount to buy must be at least %s</string>
<string name="onramp_native_payment_cumulative_limit" formatted="false">Cumulative transaction amount over %1s may require identity verification with %2s</string>
<string name="onramp_native_payment_legal_notice" formatted="false">By clicking Pay, you agree to %1s\'s %2s and %3s.</string>
<string name="onramp_no_available_providers">No available providers for this currency</string>
<string name="onramp_offer_type_fastet">Quickest processing</string>
<string name="onramp_pay_with">Pay with</string>
<string name="onramp_payment_method_subtitle">Payment method</string>
<string name="onramp_provider_max_amount">Available up to %s</string>
<string name="onramp_provider_min_amount">Available from %s</string>
<string name="onramp_provider_requirements_body">US and UK issued cards can\'t be processed via this method. Provider may require additional identity verification</string>
<string name="onramp_provider_requirements_title">Provider requirements</string>
<plurals name="onramp_providers_count">
<item quantity="one">%d provider</item>
<item quantity="other">%d providers</item>
@ -1190,10 +1202,19 @@
<string name="onramp_via_provider">via %s</string>
<string name="onramp_you_will_pay_title">You will pay</string>
<string name="organize_tokens_group">Group</string>
<string name="organize_tokens_menu_group_by_networks">Group by networks</string>
<string name="organize_tokens_menu_sort_by_balance">Sort by balance</string>
<string name="organize_tokens_sort_by_balance">By balance</string>
<string name="organize_tokens_title">Organize tokens</string>
<string name="organize_tokens_ungroup">Ungroup</string>
<string name="provider_name_support">%s support</string>
<string name="push_notification_settings_offers_updates_subtitle">Product news, exclusive offers, and activity reminders.</string>
<string name="push_notification_settings_offers_updates_title">Offers &amp; Updates</string>
<string name="push_notification_settings_price_alerts_subtitle">Get notified about price changes for top market coins.</string>
<string name="push_notification_settings_price_alerts_title">Price Alerts</string>
<string name="push_notification_settings_title">Notification Settings</string>
<string name="push_notification_settings_transaction_alerts_subtitle">Real-time alerts for transactions, exchanges, and critical updates.</string>
<string name="push_notification_settings_transaction_alerts_title">Transaction Alerts</string>
<string name="push_notifications_more_info">More info</string>
<string name="push_notifications_permission_alert_description">You can enable Notifications for Tangem in Settings.</string>
<string name="push_notifications_permission_alert_negative_button">Enable Later</string>
@ -1602,7 +1623,7 @@
<string name="swap_story_second_title">Unbeatable Rates</string>
<string name="swap_story_second_title_v2">Best Available Rate</string>
<string name="swap_story_third_subtitle">Hassle-free and intuitive, allowing you to swap tokens in just a few taps</string>
<string name="swap_story_third_subtitle_v2">Swap across major networksand thousands of tokens 0% fee on stablecoin-to-stablecoin swaps</string>
<string name="swap_story_third_subtitle_v2">Swap across major networksand thousands of tokens 0% fee on stablecoin-to-stablecoin swaps</string>
<string name="swap_story_third_title">Simply Convenient</string>
<string name="swap_story_third_title_v2">90+ Blockchains\n16,000+ Assets</string>
<string name="swap_via_provider">Swap via provider</string>
@ -1685,7 +1706,7 @@
<string name="tangempay_card_details_cvc">CVC</string>
<string name="tangempay_card_details_error_text">Failed to load data. Try again later.</string>
<string name="tangempay_card_details_expiry">Expiry</string>
<string name="tangempay_card_details_freeze_card">Freeze Card</string>
<string name="tangempay_card_details_freeze_card">Freeze card</string>
<string name="tangempay_card_details_hide_details">Hide details</string>
<string name="tangempay_card_details_hide_text">Hide</string>
<string name="tangempay_card_details_open_wallet_button">Open Google Wallet</string>
@ -1747,7 +1768,7 @@
<string name="tangempay_failed_to_issue_card">Failed to issue card</string>
<string name="tangempay_failed_to_issue_card_retry_description">A technical error has occurred, please try again by clicking the button below.</string>
<string name="tangempay_failed_to_issue_card_support_description">A technical error has occurred, please contact support.</string>
<string name="tangempay_feature_will_be_available_soon">Feature will be availible soon</string>
<string name="tangempay_feature_will_be_available_soon">Feature will be available soon</string>
<string name="tangempay_feature_will_be_available_soon_description">You will be available to issue additional cards for your payment account</string>
<string name="tangempay_get_banner_description">Get your free Tangem Visa virtual card</string>
<string name="tangempay_get_tangem_pay">Get Tangem Pay</string>
@ -1888,6 +1909,7 @@
<string name="transaction_history_transaction_validator">validator: %s</string>
<string name="transaction_notifications_warning_description">Notifications are enabled but won\'t work until you allow notifications in your device settings.</string>
<string name="transaction_notifications_warning_title">Transaction Notifications</string>
<string name="transfer_in_progress_title">Transfer in progress</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">The minimum transaction amount is %1$s.</string>
<string name="tron_will_be_send_token_fee_description">Tron network fees for popular tokens can be higher. Staking TRX may help reduce transaction costs.</string>
@ -2325,6 +2347,14 @@
<string name="xtz_withdrawal_message_ignore">No, send all</string>
<string name="xtz_withdrawal_message_reduce">Reduce by %s XTZ</string>
<string name="xtz_withdrawal_message_warning">To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ</string>
<string name="yield_apy_boost_story_first_subtitle">Activate Yield Mode for the first time and get up to 3x yield for your first 30 days</string>
<string name="yield_apy_boost_story_first_title">First month APR bonus</string>
<string name="yield_apy_boost_story_fourth_subtitle">You get market yield + Bonus. Bonus is paid once in USDT or USDC within 14 days after the 30-day period ends. Available while promo budget lasts. Terms and conditions apply</string>
<string name="yield_apy_boost_story_fourth_title">Summary</string>
<string name="yield_apy_boost_story_second_subtitle">Keep funds in Yield Mode for 30 consecutive days. Bonus is based on the yield you actually earn during that period</string>
<string name="yield_apy_boost_story_second_title">How to qualify</string>
<string name="yield_apy_boost_story_third_subtitle">3 × market yield for first 30 days\nMinimum eligibility: $1 of market yield accumulated for 30 days\nMaximum bonus: $50</string>
<string name="yield_apy_boost_story_third_title">How much you get</string>
<string name="yield_module_alert_description">When Yield Mode is active, all future top-ups to this address will be supplied to Aave. You can still manage your funds freely.</string>
<string name="yield_module_alert_title">Your %s is supplied to Aave</string>
<string name="yield_module_amount_not_transfered_to_aave_title">Supplying %1$s %2$s to Aave</string>

View file

Before

Width:  |  Height:  |  Size: 772 B

After

Width:  |  Height:  |  Size: 772 B

Before After
Before After

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="32dp"
android:viewportWidth="48"
android:viewportHeight="32">
<path
android:fillColor="#C9C9C9"
android:pathData="M4,0L44,0A4,4 0,0 1,48 4L48,28A4,4 0,0 1,44 32L4,32A4,4 0,0 1,0 28L0,4A4,4 0,0 1,4 0z" />
<path
android:fillAlpha="0.7"
android:fillColor="#ffffff"
android:fillType="evenOdd"
android:pathData="M31.459,9.589H29.991L28.89,5.26C28.838,5.061 28.727,4.885 28.564,4.802C28.157,4.593 27.708,4.427 27.219,4.344V4.177H29.583C29.909,4.177 30.154,4.427 30.195,4.718L30.766,7.841L32.233,4.177H33.66L31.459,9.589ZM34.475,9.589H33.089L34.23,4.177H35.617L34.475,9.589ZM37.41,5.676C37.451,5.384 37.696,5.218 37.981,5.218C38.43,5.176 38.919,5.259 39.326,5.467L39.571,4.302C39.163,4.135 38.715,4.052 38.307,4.052C36.962,4.052 35.983,4.801 35.983,5.842C35.983,6.633 36.677,7.049 37.166,7.299C37.696,7.549 37.9,7.716 37.859,7.965C37.859,8.34 37.451,8.507 37.044,8.507C36.554,8.507 36.065,8.382 35.617,8.173L35.372,9.339C35.862,9.547 36.391,9.631 36.881,9.631C38.389,9.672 39.326,8.923 39.326,7.799C39.326,6.383 37.41,6.3 37.41,5.676ZM44.178,9.589L43.077,4.177H41.895C41.65,4.177 41.405,4.344 41.324,4.593L39.286,9.589H40.713L40.998,8.799H42.751L42.914,9.589H44.178ZM42.098,5.634L42.506,7.674H41.364L42.098,5.634Z"
android:strokeAlpha="0.7" />
<path
android:fillColor="#ffffff"
android:pathData="M7.943,18.044C7.618,17.719 7.69,17.171 8.12,17.007C8.278,16.947 8.438,16.894 8.599,16.85C9.044,16.728 9.499,16.667 9.966,16.667C11.455,16.667 12.722,17.183 13.766,18.217C14.811,19.25 15.333,20.511 15.333,22V22.117L15.933,21.517C16.191,21.259 16.608,21.259 16.866,21.517C17.124,21.774 17.124,22.192 16.866,22.45L15.199,24.117C14.905,24.411 14.427,24.411 14.133,24.117L12.466,22.45C12.208,22.192 12.208,21.774 12.466,21.517C12.724,21.259 13.142,21.259 13.399,21.517L13.999,22.117V22C13.999,20.889 13.608,19.944 12.824,19.167C12.041,18.389 11.088,18 9.966,18C9.677,18 9.394,18.033 9.116,18.1C8.997,18.128 8.88,18.163 8.763,18.204C8.479,18.303 8.156,18.256 7.943,18.044ZM3.133,22.483C2.875,22.226 2.875,21.808 3.133,21.55L5.036,19.646C5.2,19.483 5.465,19.483 5.629,19.646L7.533,21.55C7.79,21.808 7.79,22.226 7.533,22.483C7.275,22.741 6.857,22.741 6.599,22.483L5.999,21.883V22C5.999,23.111 6.391,24.056 7.174,24.833C7.958,25.611 8.911,26 10.033,26C10.322,26 10.605,25.967 10.883,25.9C11.001,25.872 11.119,25.837 11.236,25.796C11.52,25.697 11.843,25.743 12.056,25.956C12.381,26.281 12.309,26.829 11.879,26.993C11.721,27.053 11.561,27.106 11.399,27.15C10.955,27.272 10.499,27.333 10.033,27.333C8.544,27.333 7.277,26.817 6.233,25.783C5.188,24.75 4.666,23.489 4.666,22V21.883L4.066,22.483C3.808,22.741 3.39,22.741 3.133,22.483Z" />
</vector>

View file

@ -13,6 +13,7 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -41,13 +42,22 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
override val fallback: Option<AccountList> = none()
private val logger = TangemLogger.withTag(TAG)
@OptIn(ExperimentalCoroutinesApi::class)
override fun produce(): Flow<AccountList> {
return walletAccountListFlowFactory.create(params.userWalletId)
val walletId = params.userWalletId
logger.i("produce() called for $walletId")
return walletAccountListFlowFactory.create(walletId)
.map { accountList ->
val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId)
if (userWallet.isPaymentAccountSupported()) {
accountList.plus(Account.Payment(params.userWalletId)).getOrElse { throwable ->
val userWallet = userWalletsListRepository.getSyncStrict(id = walletId)
val isPaymentSupported = userWallet.isPaymentAccountSupported()
logger.i(
"produce()[$walletId]: userWallet resolved (type=${userWallet::class.simpleName}), " +
"isPaymentAccountSupported=$isPaymentSupported",
)
if (isPaymentSupported) {
accountList.plus(Account.Payment(walletId)).getOrElse { throwable ->
error("Can not combine account list and payment account status: $throwable")
}
} else {
@ -66,4 +76,8 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
interface Factory : SingleAccountListProducer.Factory {
override fun create(params: SingleAccountListProducer.Params): DefaultSingleAccountListProducer
}
private companion object {
const val TAG = "SingleAccountListProducer"
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ -47,7 +48,15 @@ internal class WalletAccountListFlowFactory @Inject constructor(
return accountsResponseStoreFactory.create(userWallet.walletId).data
.filterNotNull()
.filter { it.accounts.isNotEmpty() }
.filter { response ->
val hasAccounts = response.accounts.isNotEmpty()
if (!hasAccounts) {
TangemLogger.e("Account list is empty for ${userWallet.walletId}")
}
hasAccounts
}
.distinctUntilChanged()
.map { converter.convert(it) }
}

View file

@ -135,10 +135,9 @@ internal class DefaultCardCryptoCurrencyFactory(
userWallet: UserWallet,
networks: Set<Network>,
): Map<Network, List<CryptoCurrency>> {
val response = walletAccountsFetcher.getSaved(userWallet.walletId)
?: return emptyMap()
val response = walletAccountsFetcher.getSaved(userWallet.walletId) ?: return emptyMap()
val existingNetworkWithCurrencies = response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
val currenciesByNetworkId = response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull()
?: return@flatMapTo emptySet()
@ -152,9 +151,11 @@ internal class DefaultCardCryptoCurrencyFactory(
accountIndex = accountIndex,
)
}
.groupBy(CryptoCurrency::network)
.groupBy { it.network.id }
return networks.associateWith { emptyList<CryptoCurrency>() } + existingNetworkWithCurrencies
return networks.associateWith { network ->
currenciesByNetworkId[network.id].orEmpty()
}
}
private suspend fun getMultiWalletCurrenciesByRawId(

View file

@ -1,6 +1,7 @@
package com.tangem.data.networks.fetcher
import arrow.core.Either
import arrow.core.right
import com.tangem.data.networks.store.NetworksStatusesStore
import com.tangem.data.networks.store.setSourceAsOnlyCache
import com.tangem.data.networks.store.storeStatus
@ -45,6 +46,13 @@ internal class CommonNetworkStatusFetcher @Inject constructor(
networkCurrencies: Set<CryptoCurrency>,
xpub: String? = null,
): Either<Throwable, Unit> {
// Guard: empty networkCurrencies would result in NetworkStatus.Verified(amounts=emptyMap()),
// which overwrites any valid cached status and leaves all currencies in this network as Loading.
if (networkCurrencies.isEmpty()) {
TangemLogger.w("Skipping fetch for $userWalletId [${network.rawId}]: networkCurrencies is empty")
return Unit.right()
}
return Either.catchOn(dispatchers.default) {
val result = withContext(dispatchers.io) {
walletManagersFacade.update(

View file

@ -73,6 +73,32 @@ internal class CommonNetworkStatusFetcherTest {
}
}
@Test
fun `fetch skips wallet manager update when networkCurrencies is empty`() = runTest {
// Arrange
val userWalletId = UserWalletId("011")
val network = cryptoCurrencyFactory.ethereum.network
// Act
val actual = fetcher.fetch(
userWalletId = userWalletId,
network = network,
networkCurrencies = emptySet(),
)
// Assert: returns success without touching the wallet manager facade — guard prevents storing
// a NetworkStatus.Verified with empty `amounts` over an existing valid cached status.
Truth.assertThat(actual).isEqualTo(Either.Right(Unit))
coVerify(inverse = true) {
walletManagersFacade.update(
userWalletId = any(),
network = any(),
extraTokens = any(),
)
}
}
@ParameterizedTest
@ProvideTestModels
fun `fetch successfully for any result of walletManagersFacade`(model: SuccessTestModel) = runTest {

View file

@ -41,6 +41,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
depositAddress = value.depositAddress,
fiatBalance = value.fiatBalance.toDM(),
cryptoBalance = value.cryptoBalance.toDM(),
availableForWithdrawal = value.availableForWithdrawal,
cards = value.cards.map { card ->
PaymentAccountStatusValueDM.TangemPayCard(
id = card.id,
@ -50,6 +51,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
adminDailyLimit = card.limit?.adminCardLimit?.amount,
isFrozen = card.isFrozen,
lastDigits = card.lastDigits,
isReissuing = card.isReissuing,
)
},
)
@ -88,6 +90,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
depositAddress = value.depositAddress,
fiatBalance = value.fiatBalance.toDomain(),
cryptoBalance = value.cryptoBalance.toDomain(),
availableForWithdrawal = value.availableForWithdrawal,
cryptoCurrency = cryptoCurrency,
cards = value.cards.map { card ->
TangemPayCard(
@ -104,6 +107,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
),
isFrozen = card.isFrozen,
lastDigits = card.lastDigits,
isReissuing = card.isReissuing,
)
},
)

View file

@ -26,6 +26,13 @@ import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.repository.*
import com.tangem.domain.pay.usecase.ChangeCardFrozenStateUseCase
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.ReissueTangemPayCardUseCase
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
import com.tangem.domain.pay.usecase.StartTangemPayOrderPollingUseCase
import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase
import com.tangem.domain.pay.usecase.*
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
@ -169,5 +176,42 @@ internal interface TangemPayDataModule {
): ProduceTangemPayInitialDataUseCase {
return ProduceTangemPayInitialDataUseCase(repository = repository)
}
@Provides
fun provideChangeCardFrozenStateUseCase(
cardDetailsRepository: TangemPayCardDetailsRepository,
startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
appCoroutineScope: AppCoroutineScope,
): ChangeCardFrozenStateUseCase {
return ChangeCardFrozenStateUseCase(
cardDetailsRepository = cardDetailsRepository,
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
appCoroutineScope = appCoroutineScope,
)
}
@Provides
@Singleton
fun provideStartTangemPayPollingUseCase(
cardDetailsRepository: TangemPayCardDetailsRepository,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
): StartTangemPayOrderPollingUseCase {
return StartTangemPayOrderPollingUseCase(cardDetailsRepository, paymentAccountStatusFetcher)
}
@Provides
fun provideReissueTangemPayCardUseCase(
reissueCardRepository: TangemPayReissueCardRepository,
startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
appCoroutineScope: AppCoroutineScope,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
): ReissueTangemPayCardUseCase {
return ReissueTangemPayCardUseCase(
reissueCardRepository = reissueCardRepository,
startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase,
appCoroutineScope = appCoroutineScope,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
)
}
}
}

View file

@ -20,6 +20,7 @@ import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayEntryPoint
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.security.DeviceSecurityInfoProvider
@ -43,6 +44,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
private val eligibilityManager: TangemPayEligibilityManager,
private val reissueCardRepository: TangemPayReissueCardRepository,
) : PaymentAccountStatusFetcher {
private val logger = TangemLogger.withTag(TAG)
@ -50,7 +52,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
override suspend fun invoke(params: PaymentAccountStatusFetcher.Params): Either<Throwable, Unit> =
Either.catchOn(dispatchers.default) {
val account = Account.Payment(userWalletId = params.userWalletId)
logger.i("fetch: ${params.userWalletId.stringValue}")
logger.i("invoke() start: ${params.userWalletId.stringValue}")
if (deviceSecurity.isSecurityExposed()) {
logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}")
@ -84,11 +86,14 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
userWalletId = params.userWalletId,
status = AccountStatus.Payment(account = account, value = status),
)
}.onLeft {
}.onLeft { throwable ->
logger.e("invoke() ${params.userWalletId} threw, falling back to ONLY_CACHE", throwable)
paymentAccountStatusesStore.updateStatusSource(
userWalletId = params.userWalletId,
source = StatusSource.ONLY_CACHE,
)
}.also { result ->
logger.i("invoke() end ${params.userWalletId}: isRight=${result.isRight()}")
}
private suspend fun proceedHasTangemPayResult(
@ -105,7 +110,12 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private suspend fun fetchTangemPayAccountStatus(account: Account.Payment): PaymentAccountStatusValue {
val prevResult = paymentAccountStatusesStore.getSyncOrNull(account.userWalletId)
logger.i(
"fetchTangemPayAccountStatus ${account.userWalletId}: " +
"prevResultType=${prevResult?.value?.let { it::class.simpleName } ?: "null"}",
)
if (prevResult == null || prevResult.value is PaymentAccountStatusValue.Error.Unavailable) {
logger.i("fetchTangemPayAccountStatus ${account.userWalletId}: writing Loading placeholder to store")
paymentAccountStatusesStore.store(
userWalletId = account.userWalletId,
status = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Loading),
@ -116,10 +126,13 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
}
private suspend fun proceedWithOrderId(account: Account.Payment): PaymentAccountStatusValue {
return if (!onboardingRepository.isTangemPayInitialDataProduced(account.userWalletId)) {
val isInitial = onboardingRepository.isTangemPayInitialDataProduced(account.userWalletId)
logger.i("proceedWithOrderId ${account.userWalletId}: isTangemPayInitialDataProduced=$isInitial")
return if (!isInitial) {
PaymentAccountStatusValue.Error.NotSynced
} else {
val orderId = onboardingRepository.getOrderId(account.userWalletId)
logger.i("proceedWithOrderId ${account.userWalletId}: orderIdPresent=${orderId != null}")
if (orderId != null) {
proceedWithOrderId(account = account, orderId = orderId)
} else {
@ -243,7 +256,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
)
}
private fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
private suspend fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
val cardInfo = this.cardInfo
val productInstance = this.productInstance
@ -278,12 +291,21 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
}
}
private fun convertToContentState(
private suspend fun convertToContentState(
userWalletId: UserWalletId,
productInstance: CustomerInfo.ProductInstance,
cardInfo: CustomerInfo.CardInfo,
customerId: String,
): PaymentAccountStatusValue {
val reissueOrder = reissueCardRepository.getReissueOrderInfo(
userWalletId = userWalletId,
cardId = productInstance.cardId,
).getOrNull()
val isReissuing = reissueOrder != null &&
reissueOrder.orderStatus != OrderStatus.CANCELED &&
reissueOrder.orderStatus != OrderStatus.COMPLETED
val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId)
return PaymentAccountStatusValue.Loaded(
source = StatusSource.ACTUAL,
@ -292,6 +314,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
depositAddress = cardInfo.depositAddress,
fiatBalance = cardInfo.fiatBalance,
cryptoBalance = cardInfo.cryptoBalance,
availableForWithdrawal = cardInfo.availableForWithdrawal,
cryptoCurrency = cryptoCurrency,
cards = listOf(
TangemPayCard(
@ -304,6 +327,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
),
isFrozen = productInstance.frozenState is TangemPayCardFrozenState.Frozen,
lastDigits = cardInfo.lastFourDigits,
isReissuing = isReissuing,
),
),
)

View file

@ -9,12 +9,17 @@ import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.flow.onStart
private const val TAG = "PaymentAccountStatusProducer"
internal class DefaultPaymentAccountStatusProducer @AssistedInject constructor(
@Assisted private val params: PaymentAccountStatusProducer.Params,
@ -24,13 +29,20 @@ internal class DefaultPaymentAccountStatusProducer @AssistedInject constructor(
) : PaymentAccountStatusProducer {
private val account = Account.Payment(userWalletId = params.userWalletId)
private val logger = TangemLogger.withTag(TAG)
override val fallback: Option<AccountStatus.Payment>
get() = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Error.Unavailable).some()
override fun produce(): Flow<AccountStatus.Payment> {
logger.i("[${params.userWalletId}] produce() called")
return paymentAccountStatusesStore.get(userWalletId = params.userWalletId)
.onEmpty { emit(value = AccountStatus.Payment(account, PaymentAccountStatusValue.NotCreated)) }
.onStart { logger.i("[${params.userWalletId}] flow subscribed to store") }
.onEach { logger.i("[${params.userWalletId}] flow emits statusType=${it.value::class.simpleName}") }
.onEmpty {
logger.i("[${params.userWalletId}] onEmpty triggered: emitting NotCreated fallback")
emit(value = AccountStatus.Payment(account, PaymentAccountStatusValue.NotCreated))
}
.flowOn(dispatchers.default)
}

View file

@ -7,12 +7,11 @@ import com.tangem.core.error.UniversalError
import com.tangem.data.pay.util.OrderStatusConverter
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.ReissueCardRequest
import com.tangem.datasource.api.pay.models.response.OrderResponse
import com.tangem.datasource.local.visa.TangemPayReissueCardStore
import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.runSuspendCatching
@ -22,6 +21,7 @@ internal class DefaultReissueCardRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val tangemPayReissueCardStore: TangemPayReissueCardStore,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
) : TangemPayReissueCardRepository {
override suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee> =
@ -53,14 +53,14 @@ internal class DefaultReissueCardRepository @Inject constructor(
override suspend fun reissueCard(
userWalletId: UserWalletId,
cardId: String,
): Either<VisaApiError, TangemPayReissueOrderInfo> = either {
): Either<VisaApiError, TangemPayOrderInfo> = either {
val response = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.reissueCard(
authHeader = authHeader,
body = ReissueCardRequest(cardId = cardId),
)
}.bind()
TangemPayReissueOrderInfo(
TangemPayOrderInfo(
orderId = response.result.orderId,
orderStatus = OrderStatusConverter.convert(response.result.status),
)
@ -77,28 +77,14 @@ internal class DefaultReissueCardRepository @Inject constructor(
override suspend fun getReissueOrderInfo(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayReissueOrderInfo?> = either {
): Either<UniversalError, TangemPayOrderInfo?> = either {
val orderId = runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull()
if (orderId == null) {
return null.right()
}
val order = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader, orderId)
}.bind()
val result = order.result ?: raise(VisaApiError.Unspecified)
TangemPayReissueOrderInfo(
orderId = result.id,
orderStatus = when (result.status) {
OrderResponse.Result.Status.NEW -> OrderStatus.NEW
OrderResponse.Result.Status.PROCESSING -> OrderStatus.PROCESSING
OrderResponse.Result.Status.COMPLETED -> OrderStatus.COMPLETED
OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED
},
)
cardDetailsRepository.getOrderInfo(userWalletId, orderId).bind()
}
private companion object {

View file

@ -3,6 +3,7 @@ package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.right
import com.tangem.core.error.UniversalError
import com.tangem.data.pay.util.RainCryptoUtil
@ -22,22 +23,17 @@ import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayCardDetails
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
private const val TAG = "TangemPay: CardDetailsRepository"
@ -51,12 +47,8 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
private val storage: TangemPayStorage,
private val cardFrozenStateStore: TangemPayCardFrozenStateStore,
private val errorConverter: TangemPayErrorConverter,
private val pollingScope: AppCoroutineScope,
) : TangemPayCardDetailsRepository {
private val pollingJobs = mutableMapOf<String, Job>()
private val storePollingMutex = Mutex()
override suspend fun getCardBalance(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardBalance> {
return catch(
block = {
@ -201,123 +193,43 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
override suspend fun freezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> {
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Pending)
return requestHelper.performRequest(userWalletId) {
): Either<UniversalError, TangemPayOrderInfo> = either {
val response = requestHelper.performRequest(userWalletId) {
tangemPayApi.freezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId))
}.onLeft {
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Unfrozen)
}.map { response ->
val state = when (response.result?.status) {
FreezeUnfreezeCardResponse.Status.COMPLETED -> TangemPayCardFrozenState.Frozen
FreezeUnfreezeCardResponse.Status.NEW,
FreezeUnfreezeCardResponse.Status.PROCESSING,
-> TangemPayCardFrozenState.Pending
FreezeUnfreezeCardResponse.Status.CANCELED,
null,
-> TangemPayCardFrozenState.Unfrozen
}
if (state == TangemPayCardFrozenState.Pending) {
startOrderIdPolling(
userWalletId = userWalletId,
cardId = cardId,
orderId = response.result?.orderId,
isFreeze = true,
)
}
cardFrozenStateStore.store(cardId, state)
}.bind()
state
}
val result = response.result ?: raise(VisaApiError.Unspecified)
TangemPayOrderInfo(
orderId = result.orderId,
orderStatus = when (result.status) {
FreezeUnfreezeCardResponse.Status.NEW -> OrderStatus.NEW
FreezeUnfreezeCardResponse.Status.PROCESSING -> OrderStatus.PROCESSING
FreezeUnfreezeCardResponse.Status.COMPLETED -> OrderStatus.COMPLETED
FreezeUnfreezeCardResponse.Status.CANCELED -> OrderStatus.CANCELED
},
)
}
override suspend fun unfreezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> {
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Pending)
return requestHelper.performRequest(userWalletId) {
): Either<UniversalError, TangemPayOrderInfo> = either {
val response = requestHelper.performRequest(userWalletId) {
tangemPayApi.unfreezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId))
}.onLeft {
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Frozen)
}.map { response ->
val state = when (response.result?.status) {
FreezeUnfreezeCardResponse.Status.COMPLETED -> TangemPayCardFrozenState.Unfrozen
FreezeUnfreezeCardResponse.Status.NEW,
FreezeUnfreezeCardResponse.Status.PROCESSING,
-> TangemPayCardFrozenState.Pending
FreezeUnfreezeCardResponse.Status.CANCELED,
null,
-> TangemPayCardFrozenState.Frozen
}
if (state == TangemPayCardFrozenState.Pending) {
startOrderIdPolling(
userWalletId = userWalletId,
cardId = cardId,
orderId = response.result?.orderId,
isFreeze = false,
)
}
cardFrozenStateStore.store(cardId, state)
}.bind()
state
}
}
val result = response.result ?: raise(VisaApiError.Unspecified)
private suspend fun startOrderIdPolling(
userWalletId: UserWalletId,
cardId: String,
orderId: String?,
isFreeze: Boolean,
) {
if (orderId.isNullOrEmpty()) return
storePollingMutex.withLock {
if (pollingJobs.containsKey(orderId)) return
val pollingJob = pollingScope.launch {
try {
var retryCount = 0
while (isActive && pollingJobs.containsKey(orderId)) {
delay(duration = 5.seconds)
val orderStatus = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader, orderId)
}
orderStatus.onRight { response ->
val status = response.result?.status
if (status == Status.COMPLETED || status == Status.CANCELED) {
// Remove from jobs
pollingJobs.remove(key = orderId)
// Final card state
val finalState = when {
status == Status.COMPLETED && isFreeze
-> TangemPayCardFrozenState.Frozen
status == Status.COMPLETED && !isFreeze
-> TangemPayCardFrozenState.Unfrozen
else -> return@launch
}
cardFrozenStateStore.store(cardId, finalState)
}
}.onLeft { error ->
TangemLogger.e("error ${error.errorCode}")
// stop retrying after 3 errors
if (retryCount > MAX_POLLING_RETRIES) {
pollingJobs.remove(key = orderId)
}
}
retryCount++
}
} catch (e: Exception) {
TangemLogger.e("Error", e)
storePollingMutex.withLock {
pollingJobs.remove(orderId)
}
}
}
pollingJobs[orderId] = pollingJob
}
TangemPayOrderInfo(
orderId = result.orderId,
orderStatus = when (result.status) {
FreezeUnfreezeCardResponse.Status.NEW -> OrderStatus.NEW
FreezeUnfreezeCardResponse.Status.PROCESSING -> OrderStatus.PROCESSING
FreezeUnfreezeCardResponse.Status.COMPLETED -> OrderStatus.COMPLETED
FreezeUnfreezeCardResponse.Status.CANCELED -> OrderStatus.CANCELED
},
)
}
override suspend fun updateCardDisplayName(
@ -376,6 +288,31 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
return cardFrozenStateStore.getSyncOrNull(cardId)
}
override suspend fun setCardFrozenState(cardId: String, state: TangemPayCardFrozenState) {
cardFrozenStateStore.store(cardId, state)
}
override suspend fun getOrderInfo(
userWalletId: UserWalletId,
orderId: String,
): Either<UniversalError, TangemPayOrderInfo> = either {
val order = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getOrder(authHeader, orderId)
}.bind()
val result = order.result ?: raise(VisaApiError.Unspecified)
TangemPayOrderInfo(
orderId = result.id,
orderStatus = when (result.status) {
Status.NEW -> OrderStatus.NEW
Status.PROCESSING -> OrderStatus.PROCESSING
Status.COMPLETED -> OrderStatus.COMPLETED
Status.CANCELED -> OrderStatus.CANCELED
},
)
}
private suspend fun getPublicKeyBase64(): String {
val env = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemPay).environment
return when (env) {
@ -395,8 +332,4 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
TangemLogger.withTag(TAG).e("Error", throwable)
return errorConverter.convert(throwable).left()
}
private companion object {
const val MAX_POLLING_RETRIES = 3
}
}

View file

@ -16,11 +16,15 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
internal typealias WalletIdWithPaymentStatus = Map<String, AccountStatus.Payment>
internal typealias WalletIdWithPaymentStatusDM = Map<String, PaymentAccountStatusValueDM>
private const val TAG = "PaymentAccountStatusesStore"
/**
* Store for payment account statuses with dual storage (runtime + persistence).
*
@ -34,10 +38,18 @@ internal class PaymentAccountStatusesStore(
scope: AppCoroutineScope,
) {
private val logger = TangemLogger.withTag(TAG)
init {
scope.launch {
logger.i("init: loading cached payment statuses from persistence")
try {
val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch
val cachedStatuses = persistenceDataStore.data.firstOrNull()
if (cachedStatuses == null) {
logger.i("init: persistence empty (firstOrNull == null), runtimeStore stays empty")
return@launch
}
logger.i("init: loaded ${cachedStatuses.size} cached entries; populating runtimeStore")
runtimeStore.store(
value = cachedStatuses.mapValues { (rawUserWalletId, statusDM) ->
val account = Account.Payment(userWalletId = UserWalletId(rawUserWalletId))
@ -45,19 +57,33 @@ internal class PaymentAccountStatusesStore(
AccountStatus.Payment(account = account, value = statusValue)
},
)
logger.i("init: runtimeStore populated with ${cachedStatuses.size} entries")
} catch (e: Exception) {
runSuspendCatching { persistenceDataStore.updateData { emptyMap() } }
TangemLogger.e("Error while loading cached payment account statuses", e)
logger.e("Error while loading cached payment account statuses", e)
}
}
}
fun get(userWalletId: UserWalletId): Flow<AccountStatus.Payment> {
return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] }
return runtimeStore.get()
.onStart { logger.i("get($userWalletId): subscribed to runtimeStore") }
.onEach { map ->
logger.i(
"get($userWalletId): runtimeStore emitted map size=${map.size}, " +
"hasEntry=${map.containsKey(userWalletId.stringValue)}",
)
}
.mapNotNull { it[userWalletId.stringValue] }
.onEach { status ->
logger.i("get($userWalletId): emitting statusType=${status.value::class.simpleName}")
}
}
suspend fun getSyncOrNull(userWalletId: UserWalletId): AccountStatus.Payment? {
return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue)
val result = runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue)
logger.i("getSyncOrNull($userWalletId) valueType=${result?.value?.let { it::class.simpleName } ?: "null"}")
return result
}
suspend fun updateStatusSource(userWalletId: UserWalletId, source: StatusSource) {
@ -71,6 +97,7 @@ internal class PaymentAccountStatusesStore(
}
suspend fun store(userWalletId: UserWalletId, status: AccountStatus.Payment) {
logger.i("store($userWalletId): valueType=${status.value::class.simpleName}")
coroutineScope {
launch { storeInRuntime(userWalletId = userWalletId, status = status) }
launch { storeInPersistence(userWalletId = userWalletId, status = status.value) }

View file

@ -15,6 +15,7 @@ import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.Status
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, CustomerInfo> {
@Suppress("ComplexCondition")
@ -33,6 +34,7 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
isPinSet = value.card?.isPinSet == true,
fiatBalance = fiatBalance.toDomain(),
cryptoBalance = cryptoBalance.toDomain(),
availableForWithdrawal = value.balance?.availableForWithdrawal?.amount.orZero(),
)
} else {
null

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayCardDetails
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import kotlinx.coroutines.flow.Flow
@ -65,12 +66,12 @@ internal class MockAwareTangemPayCardDetailsRepository @Inject constructor(
override suspend fun freezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> = real.freezeCard(userWalletId, cardId)
): Either<UniversalError, TangemPayOrderInfo> = real.freezeCard(userWalletId, cardId)
override suspend fun unfreezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> = real.unfreezeCard(userWalletId, cardId)
): Either<UniversalError, TangemPayOrderInfo> = real.unfreezeCard(userWalletId, cardId)
override fun cardFrozenState(cardId: String): Flow<TangemPayCardFrozenState> =
real.cardFrozenState(cardId)
@ -78,6 +79,14 @@ internal class MockAwareTangemPayCardDetailsRepository @Inject constructor(
override suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState? =
real.cardFrozenStateSync(cardId)
override suspend fun setCardFrozenState(cardId: String, state: TangemPayCardFrozenState) =
real.setCardFrozenState(cardId, state)
override suspend fun getOrderInfo(
userWalletId: UserWalletId,
orderId: String,
): Either<UniversalError, TangemPayOrderInfo> = real.getOrderInfo(userWalletId, orderId)
override suspend fun updateCardDisplayName(
cardId: String,
userWalletId: UserWalletId,

View file

@ -7,6 +7,7 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.formatHex
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.TxSentFrom
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType
@ -91,6 +92,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
token = network.currencySymbol,
feeType = null,
feeToken = network.currencySymbol,
feeAssetType = AnalyticsParam.FeeAssetType.Coin,
),
memoType = MemoType.Null,
),

View file

@ -46,6 +46,7 @@ import com.tangem.domain.tokens.operations.TokenListFactory
import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -73,7 +74,7 @@ import java.math.BigDecimal
[REDACTED_AUTHOR]
*/
// TODO: Move to :data:account:status [REDACTED_JIRA]
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
@OptIn(ExperimentalCoroutinesApi::class)
internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor(
@Assisted private val params: SingleAccountStatusListProducer.Params,
@ -90,17 +91,29 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : SingleAccountStatusListProducer {
private val logger = TangemLogger.withTag(TAG)
override val fallback: Option<AccountStatusList> = none()
override fun produce(): Flow<AccountStatusList> {
logger.i("produce() called for ${params.userWalletId}")
return flattenFlow()
.onEach { list ->
logger.i(
"produce()[${params.userWalletId}] emit: accounts=${list.accountStatuses.size}, " +
"currencies=${list.flattenCurrencies().size}, " +
"totalFiatType=${list.totalFiatBalance::class.simpleName}",
)
}
.flowOn(dispatchers.default)
}
@Suppress("LongMethod")
private fun flattenFlow(): Flow<AccountStatusList> = channelFlow {
val walletId = params.userWalletId
logger.i("flattenFlow[$walletId]: start")
val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId)
logger.i("flattenFlow[$walletId]: userWallet resolved (type=${userWallet::class.simpleName})")
val flattenCurrency: MutableSharedFlow<Map<AccountCurrencyId, CryptoCurrency>> = MutableSharedFlow(
replay = 1,
@ -108,11 +121,20 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
)
val accountListFlow: StateFlow<AccountList> = singleAccountListSupplier(walletId)
.onEach { accountList -> flattenCurrency.tryEmit(accountList.flattenMapCurrencies()) }
.onEach { accountList ->
logger.i(
"flattenFlow[$walletId]: accountList emitted accounts=${accountList.accounts.size}, " +
"currencies=${accountList.flattenMapCurrencies().size}",
)
flattenCurrency.tryEmit(accountList.flattenMapCurrencies())
}
.stateIn(this)
val hasCachedNetworks = networksRepository.hasCachedStatuses(walletId)
logger.i("flattenFlow[$walletId]: hasCachedNetworks=$hasCachedNetworks")
if (!hasCachedNetworks) {
val initialAccounts = accountListFlow.value.accounts.size
logger.i("flattenFlow[$walletId]: sending Loading placeholder (accounts=$initialAccounts)")
send(createLoadingAccountStatusList(accountListFlow.value))
}
@ -121,11 +143,19 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
flattenCurrency = flattenCurrency,
)
if (userWallet.isPaymentAccountSupported()) {
val isPaymentSupported = userWallet.isPaymentAccountSupported()
logger.i("flattenFlow[$walletId]: isPaymentAccountSupported=$isPaymentSupported")
if (isPaymentSupported) {
combineWithPaymentAccount(
accountListFlow = accountListFlow,
cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow,
paymentAccountStatusFlow = paymentAccountStatusSupplier.invoke(userWalletId = params.userWalletId),
paymentAccountStatusFlow = paymentAccountStatusSupplier.invoke(userWalletId = params.userWalletId)
.onEach { paymentStatus ->
logger.i(
"flattenFlow[$walletId]: paymentAccountStatus emitted " +
"valueType=${paymentStatus.value::class.simpleName}",
)
},
)
} else {
combineWithoutPaymentAccount(
@ -146,6 +176,12 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
flow2 = cryptoCurrencyStatusFlow,
flow3 = paymentAccountStatusFlow,
transform = { accountList, currencyStatusMap, paymentAccountStatus ->
logger.i(
"combineWithPayment[${params.userWalletId}] transform: " +
"accounts=${accountList.accounts.size}, " +
"currencyStatusMap=${currencyStatusMap.size}, " +
"paymentType=${paymentAccountStatus.value::class.simpleName}",
)
val accountStatuses = accountList.accounts.map { account ->
when (account) {
is Account.Payment -> paymentAccountStatus
@ -192,6 +228,11 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
flow = accountListFlow,
flow2 = cryptoCurrencyStatusFlow,
transform = { accountList, currencyStatusMap ->
logger.i(
"combineWithoutPayment[${params.userWalletId}] transform: " +
"accounts=${accountList.accounts.size}, " +
"currencyStatusMap=${currencyStatusMap.size}",
)
val accountStatuses = accountList.accounts
.filterIsInstance<Account.CryptoPortfolio>()
.map { account ->
@ -242,14 +283,18 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
): Flow<Map<AccountCurrencyId, CryptoCurrencyStatus>> {
val walletId = userWallet.walletId
val networkStatusFlow: SharedFlow<Map<Network.ID, NetworkStatus>> = networkStatusFlow(walletId)
.onEach { logger.i("flattenCurrencyStatusFlow[$walletId]: networkStatuses emitted size=${it.size}") }
.shareIn(this, started = SharingStarted.Eagerly, replay = 1)
val stakingBalanceFlow: SharedFlow<Map<StakingID, Set<StakingBalance>>> = stakingFlow(userWallet)
.onEach { logger.i("flattenCurrencyStatusFlow[$walletId]: stakingBalances emitted stakingIds=${it.size}") }
.shareIn(this, started = SharingStarted.Eagerly, replay = 1)
val quoteStatusFlow: SharedFlow<Map<CryptoCurrency.RawID, QuoteStatus>> = quoteStatusFlow()
.onEach { logger.i("flattenCurrencyStatusFlow[$walletId]: quoteStatuses emitted size=${it.size}") }
.shareIn(this, started = SharingStarted.Eagerly, replay = 1)
return flattenCurrency
.distinctUntilChanged()
.onEach { logger.i("flattenCurrencyStatusFlow[$walletId]: flattenCurrency emitted size=${it.size}") }
.flatMapLatest { a ->
combine(
flow = networkStatusFlow,
@ -264,6 +309,15 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
)
}
.distinctUntilChanged()
.onEach { box ->
logger.i(
"flattenCurrencyStatusFlow[$walletId]: box emitted " +
"currencies=${box.flattenCurrencyMap.size}, " +
"networks=${box.networkStatusMap.size}, " +
"stakings=${box.stakingBalanceMap.size}, " +
"quotes=${box.quoteStatusMap.size}",
)
}
.map { box ->
val flattenCurrencyMap: Map<AccountCurrencyId, CryptoCurrency> = box.flattenCurrencyMap
val networkStatusMap: Map<Network.ID, NetworkStatus> = box.networkStatusMap
@ -402,4 +456,8 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
interface Factory : SingleAccountStatusListProducer.Factory {
override fun create(params: SingleAccountStatusListProducer.Params): DefaultSingleAccountStatusListProducer
}
private companion object {
const val TAG = "SingleAccountStatusListProducer"
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
@ -27,27 +28,51 @@ class IsAccountsModeEnabledUseCase(
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(): Flow<Boolean> {
TangemLogger.i("$TAG: invoke() started")
val cryptoMode = multiAccountListSupplier.invoke()
.map { lists -> lists.any { it.hasMultipleCryptoPortfolios() } }
.onEach { lists ->
TangemLogger.i("$TAG: multiAccountListSupplier emitted ${lists.size} lists (cryptoMode branch)")
}
.map { lists ->
val isCryptoMode = lists.any { it.hasMultipleCryptoPortfolios() }
TangemLogger.i("$TAG: cryptoMode=$isCryptoMode")
isCryptoMode
}
val paymentMode = multiAccountListSupplier.invoke().flatMapLatest { lists ->
val walletIdsWithPayment = lists.mapNotNull { list ->
if (list.accounts.any { it is Account.Payment }) list.userWalletId else null
}
TangemLogger.i("$TAG: walletIdsWithPayment=${walletIdsWithPayment.size}")
if (walletIdsWithPayment.isEmpty()) {
flowOf(false)
} else {
val flows = walletIdsWithPayment.map { walletId ->
paymentAccountStatusSupplier.invoke(walletId)
.onEach { status ->
TangemLogger.i("$TAG: paymentStatus for $walletId = ${status.value::class.simpleName}")
}
.map { it.value.isActivePayment() }
.onStart { emit(false) }
.onStart {
TangemLogger.i("$TAG: paymentAccountStatusSupplier onStart for $walletId")
emit(false)
}
}
combine(flows) { results ->
val isPaymentMode = results.any { it }
TangemLogger.i("$TAG: paymentMode combine result=$isPaymentMode (${results.toList()})")
isPaymentMode
}
combine(flows) { results -> results.any { it } }
}
}
return combine(cryptoMode, paymentMode) { crypto, payment -> crypto || payment }
return combine(cryptoMode, paymentMode) { crypto, payment ->
val isEnabled = crypto || payment
TangemLogger.i("$TAG: final combine crypto=$crypto, payment=$payment, result=$isEnabled")
isEnabled
}
.distinctUntilChanged()
}
@ -89,5 +114,6 @@ class IsAccountsModeEnabledUseCase(
private companion object {
const val PAYMENT_STATUS_SYNC_TIMEOUT_MS = 1_000L
const val TAG = "IsAccountsModeEnabledUseCase"
}
}

View file

@ -22,20 +22,27 @@ sealed class IntroductionProcess(
* Tracks opening the Create Wallet introduction screen.
*/
class CreateWalletIntroScreenOpened(
screenType: ScreenType,
referralId: String?,
) : IntroductionProcess(
event = "Create Wallet Intro Screen Opened",
params = buildMap {
put(AnalyticsParam.SCREEN_TYPE, screenType.value)
putAll(getReferralParams(referralId))
},
), CriticalEvent
), CriticalEvent {
enum class ScreenType(val value: String) {
Cold("Cold Wallet"),
Hot("Mobile Wallet"),
}
}
class ButtonScanCard(
val source: AnalyticsParam.ScreensSources,
) : IntroductionProcess(
event = "Button - Scan Card",
params = mapOf(
AnalyticsParam.Key.SOURCE to source.value,
AnalyticsParam.SOURCE to source.value,
),
)
}

View file

@ -138,6 +138,7 @@ sealed class PaymentAccountStatusValue {
* @property depositAddress The address for deposits, if available.
* @property fiatBalance The fiat balance details.
* @property cryptoBalance The crypto balance details.
* @property availableForWithdrawal The crypto amount currently available for withdrawal/swap (excludes pending/locked funds).
* @property cards The list of user's cards.
*/
@Serializable
@ -148,13 +149,14 @@ sealed class PaymentAccountStatusValue {
val depositAddress: String?,
val fiatBalance: FiatBalance,
val cryptoBalance: CryptoBalance,
val availableForWithdrawal: SerializedBigDecimal,
val cryptoCurrency: CryptoCurrency.Token,
val cards: List<TangemPayCard>,
) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency,
value = CryptoCurrencyStatus.Loaded(
amount = cryptoBalance.balance,
amount = availableForWithdrawal,
fiatAmount = fiatBalance.availableBalance,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,

View file

@ -22,4 +22,5 @@ data class TangemPayCard(
@SerialName("limit") val limit: TangemPayCardLimitData?,
@SerialName("is_frozen") val isFrozen: Boolean,
@SerialName("last_digits") val lastDigits: String,
@SerialName("is_reissuing") val isReissuing: Boolean,
)

View file

@ -46,11 +46,13 @@ sealed class StakingAnalyticsEvent(
data class StakeInProgressScreenOpened(
val validator: String,
val action: StakingActionType,
val feeAssetType: AnalyticsParam.FeeAssetType,
) : StakingAnalyticsEvent(
event = "Stake In Progress Screen Opened",
params = mapOf(
"Validator" to validator,
"Action" to action.asAnalyticName,
AnalyticsParam.Key.FEE_ASSET_TYPE to feeAssetType.value,
),
), AppsFlyerIncludedEvent

View file

@ -9,6 +9,10 @@ android {
namespace = "com.tangem.domain.visa"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Project - Core */
api(projects.core.pagination)
@ -35,4 +39,12 @@ dependencies {
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
ksp(deps.moshi.kotlin.codegen)
/** Tests */
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
testImplementation(projects.common.test)
}

View file

@ -83,5 +83,6 @@ data class CustomerInfo(
val isPinSet: Boolean,
val fiatBalance: PaymentAccountStatusValue.FiatBalance,
val cryptoBalance: PaymentAccountStatusValue.CryptoBalance,
val availableForWithdrawal: BigDecimal,
)
}

View file

@ -5,4 +5,7 @@ enum class OrderStatus {
PROCESSING,
COMPLETED,
CANCELED,
}
}
val OrderStatus.isFinalStatus
get() = this == OrderStatus.COMPLETED || this == OrderStatus.CANCELED

View file

@ -1,6 +1,6 @@
package com.tangem.domain.pay.model
data class TangemPayReissueOrderInfo(
data class TangemPayOrderInfo(
val orderId: String,
val orderStatus: OrderStatus,
)

View file

@ -7,6 +7,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayCardDetails
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import kotlinx.coroutines.flow.Flow
@ -24,14 +25,12 @@ interface TangemPayCardDetailsRepository {
suspend fun setAddToWalletAsDone(userWalletId: UserWalletId): Either<UniversalError, Unit>
suspend fun freezeCard(userWalletId: UserWalletId, cardId: String): Either<UniversalError, TangemPayCardFrozenState>
suspend fun unfreezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState>
suspend fun freezeCard(userWalletId: UserWalletId, cardId: String): Either<UniversalError, TangemPayOrderInfo>
suspend fun unfreezeCard(userWalletId: UserWalletId, cardId: String): Either<UniversalError, TangemPayOrderInfo>
fun cardFrozenState(cardId: String): Flow<TangemPayCardFrozenState>
suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState?
suspend fun setCardFrozenState(cardId: String, state: TangemPayCardFrozenState)
suspend fun updateCardDisplayName(
cardId: String,
@ -44,4 +43,6 @@ interface TangemPayCardDetailsRepository {
userWalletId: UserWalletId,
limit: String,
): Either<UniversalError, Unit>
suspend fun getOrderInfo(userWalletId: UserWalletId, orderId: String): Either<UniversalError, TangemPayOrderInfo>
}

View file

@ -4,19 +4,19 @@ import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.pay.TangemPayReissueCardFee
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.visa.error.VisaApiError
interface TangemPayReissueCardRepository {
suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee>
suspend fun reissueCard(userWalletId: UserWalletId, cardId: String): Either<VisaApiError, TangemPayReissueOrderInfo>
suspend fun reissueCard(userWalletId: UserWalletId, cardId: String): Either<VisaApiError, TangemPayOrderInfo>
suspend fun storeReissueOrderId(cardId: String, orderId: String): Either<UniversalError, Unit>
suspend fun getReissueOrderInfo(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayReissueOrderInfo?>
): Either<UniversalError, TangemPayOrderInfo?>
}

View file

@ -0,0 +1,45 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.utils.coroutines.AppCoroutineScope
import kotlinx.coroutines.async
class ChangeCardFrozenStateUseCase(
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
private val appCoroutineScope: AppCoroutineScope,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
cardId: String,
isFreezing: Boolean,
): Either<UniversalError, Unit> {
val successState = if (isFreezing) TangemPayCardFrozenState.Frozen else TangemPayCardFrozenState.Unfrozen
val failState = if (isFreezing) TangemPayCardFrozenState.Unfrozen else TangemPayCardFrozenState.Frozen
return either {
cardDetailsRepository.setCardFrozenState(cardId, TangemPayCardFrozenState.Pending)
val order = if (isFreezing) {
cardDetailsRepository.freezeCard(userWalletId, cardId).bind()
} else {
cardDetailsRepository.unfreezeCard(userWalletId, cardId).bind()
}
val isCompleted = appCoroutineScope.async {
val isCompleted = startTangemPayOrderPollingUseCase(order, userWalletId)
cardDetailsRepository.setCardFrozenState(cardId, if (isCompleted) successState else failState)
isCompleted
}.await()
if (!isCompleted) raise(VisaApiError.Unspecified)
}.onLeft {
cardDetailsRepository.setCardFrozenState(cardId, failState)
}
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.AppCoroutineScope
import kotlinx.coroutines.launch
class ReissueTangemPayCardUseCase(
private val reissueCardRepository: TangemPayReissueCardRepository,
private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
private val appCoroutineScope: AppCoroutineScope,
) {
suspend operator fun invoke(userWalletId: UserWalletId, cardId: String): Either<VisaApiError, Unit> = either {
val order = reissueCardRepository.reissueCard(userWalletId, cardId).bind()
if (order.orderStatus == OrderStatus.CANCELED) {
raise(VisaApiError.Unspecified)
}
reissueCardRepository.storeReissueOrderId(cardId, order.orderId)
paymentAccountStatusFetcher.invoke(userWalletId)
appCoroutineScope.launch {
startTangemPayOrderPollingUseCase(order, userWalletId)
}
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.domain.pay.usecase
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.model.isFinalStatus
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import kotlinx.coroutines.delay
class StartTangemPayOrderPollingUseCase(
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
) {
suspend operator fun invoke(order: TangemPayOrderInfo, userWalletId: UserWalletId): Boolean {
while (true) {
val newOrder = if (order.orderStatus.isFinalStatus) {
order
} else {
cardDetailsRepository.getOrderInfo(userWalletId, order.orderId).getOrNull()
}
if (newOrder != null && newOrder.orderStatus.isFinalStatus) {
paymentAccountStatusFetcher.invoke(userWalletId)
return newOrder.orderStatus == OrderStatus.COMPLETED
}
delay(POLLING_DELAY)
}
}
companion object {
private const val POLLING_DELAY = 3000L
}
}

View file

@ -0,0 +1,107 @@
package com.tangem.domain.pay.usecase
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth.assertThat
import com.tangem.common.test.TestAppCoroutineScope
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.mockk
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
internal class ChangeCardFrozenStateUseCaseTest {
private val cardDetailsRepository: TangemPayCardDetailsRepository = mockk(relaxUnitFun = true)
private val startPollingUseCase: StartTangemPayOrderPollingUseCase = mockk()
@Test
fun `GIVEN freezeCard fails WHEN invoke with isFreezing=true THEN sets Pending then Unfrozen and returns Left`() =
runTest {
val useCase = createUseCase()
coEvery {
cardDetailsRepository.freezeCard(USER_WALLET_ID, CARD_ID)
} returns VisaApiError.Unspecified.left()
val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = true)
assertThat(result.isLeft()).isTrue()
coVerifyOrder {
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending)
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Unfrozen)
}
coVerify(exactly = 0) { startPollingUseCase(any(), any()) }
}
@Test
fun `GIVEN unfreezeCard fails WHEN invoke with isFreezing=false THEN sets Pending then Frozen and returns Left`() =
runTest {
val useCase = createUseCase()
coEvery {
cardDetailsRepository.unfreezeCard(USER_WALLET_ID, CARD_ID)
} returns VisaApiError.Unspecified.left()
val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = false)
assertThat(result.isLeft()).isTrue()
coVerifyOrder {
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending)
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Frozen)
}
coVerify(exactly = 0) { startPollingUseCase(any(), any()) }
}
@Test
fun `GIVEN freeze succeeds and order COMPLETED WHEN invoke with isFreezing=true THEN sets Frozen and returns Right`() =
runTest {
val useCase = createUseCase()
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED)
coEvery { cardDetailsRepository.freezeCard(USER_WALLET_ID, CARD_ID) } returns order.right()
coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true
val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = true)
assertThat(result.isRight()).isTrue()
coVerifyOrder {
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending)
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Frozen)
}
}
@Test
fun `GIVEN unfreeze succeeds and order COMPLETED WHEN invoke with isFreezing=false THEN sets Unfrozen and returns Right`() =
runTest {
val useCase = createUseCase()
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED)
coEvery { cardDetailsRepository.unfreezeCard(USER_WALLET_ID, CARD_ID) } returns order.right()
coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true
val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = false)
assertThat(result.isRight()).isTrue()
coVerifyOrder {
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending)
cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Unfrozen)
}
}
private fun TestScope.createUseCase() = ChangeCardFrozenStateUseCase(
cardDetailsRepository = cardDetailsRepository,
startTangemPayOrderPollingUseCase = startPollingUseCase,
appCoroutineScope = TestAppCoroutineScope(this),
)
private companion object {
val USER_WALLET_ID = UserWalletId("aabbcc112233")
const val CARD_ID = "card-test-id"
const val ORDER_ID = "order-test-1"
}
}

View file

@ -0,0 +1,119 @@
package com.tangem.domain.pay.usecase
import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.error.VisaApiError
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
internal class StartTangemPayOrderPollingUseCaseTest {
private val cardDetailsRepository: TangemPayCardDetailsRepository = mockk()
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk()
private val useCase = StartTangemPayOrderPollingUseCase(
cardDetailsRepository = cardDetailsRepository,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
)
@Test
fun `GIVEN order already COMPLETED WHEN invoke THEN returns true without polling`() = runTest {
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED)
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
val result = useCase(order, USER_WALLET_ID)
assertThat(result).isTrue()
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
coVerify(exactly = 0) { cardDetailsRepository.getOrderInfo(any(), any()) }
}
@Test
fun `GIVEN order already CANCELED WHEN invoke THEN returns false without polling`() = runTest {
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.CANCELED)
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
val result = useCase(order, USER_WALLET_ID)
assertThat(result).isFalse()
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
coVerify(exactly = 0) { cardDetailsRepository.getOrderInfo(any(), any()) }
}
@Test
fun `GIVEN processing order WHEN poll returns COMPLETED THEN returns true and fetches status`() = runTest {
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
coEvery {
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
} returns TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED).right()
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
val result = useCase(order, USER_WALLET_ID)
assertThat(result).isTrue()
coVerify(exactly = 1) { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) }
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
}
@Test
fun `GIVEN processing order WHEN poll returns CANCELED THEN returns false and fetches status`() = runTest {
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
coEvery {
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
} returns TangemPayOrderInfo(ORDER_ID, OrderStatus.CANCELED).right()
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
val result = useCase(order, USER_WALLET_ID)
assertThat(result).isFalse()
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
}
@Test
fun `GIVEN new order WHEN getOrderInfo fails once then returns COMPLETED THEN returns true after two polls`() = runTest {
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.NEW)
coEvery {
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
} returnsMany listOf(
VisaApiError.Unspecified.left(),
TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED).right(),
)
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
val result = useCase(order, USER_WALLET_ID)
assertThat(result).isTrue()
coVerify(exactly = 2) { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) }
}
@Test
fun `GIVEN processing order WHEN multiple non-final polls then COMPLETED THEN returns true after all polls`() = runTest {
val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING)
coEvery {
cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID)
} returnsMany listOf(
TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING).right(),
TangemPayOrderInfo(ORDER_ID, OrderStatus.NEW).right(),
TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED).right(),
)
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
val result = useCase(order, USER_WALLET_ID)
assertThat(result).isTrue()
coVerify(exactly = 3) { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) }
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
}
private companion object {
val USER_WALLET_ID = UserWalletId("aabbcc112233")
const val ORDER_ID = "order-test-1"
}
}

View file

@ -157,6 +157,7 @@ sealed class WcAnalyticEvents(
network: Network,
securityStatus: CheckDAppResult,
accountDerivation: Int?,
feeAssetType: AnalyticsParam.FeeAssetType = AnalyticsParam.FeeAssetType.Coin,
) : WcAnalyticEvents(
event = "Signature Request Handled",
params = buildMap {
@ -168,6 +169,7 @@ sealed class WcAnalyticEvents(
accountDerivation?.let {
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
}
put(AnalyticsParam.Key.FEE_ASSET_TYPE, feeAssetType.value)
},
), AppsFlyerIncludedEvent

View file

@ -18,6 +18,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.utils.parseBigDecimalOrNull
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.transaction.error.GetFeeError
@ -265,7 +266,13 @@ internal class GiveApprovalModel @Inject constructor(
private fun sendApproveSuccessAnalytics(feeContent: FeeSelectorUM.Content) {
val currency = params.cryptoCurrencyStatus.currency
val feeToken = feeContent.feeExtraInfo.feeCryptoCurrencyStatus.currency.symbol
val feeCurrency = feeContent.feeExtraInfo.feeCryptoCurrencyStatus.currency
val feeToken = feeCurrency.symbol
val feeAssetType = if (feeCurrency is CryptoCurrency.Coin) {
AnalyticsParam.FeeAssetType.Coin
} else {
AnalyticsParam.FeeAssetType.Token
}
val permissionType = when (uiState.value.approveType) {
ApproveType.LIMITED -> "Current transaction"
ApproveType.UNLIMITED -> "Unlimited"
@ -275,6 +282,7 @@ internal class GiveApprovalModel @Inject constructor(
token = currency.symbol,
feeType = feeContent.toAnalyticType(),
feeToken = feeToken,
feeAssetType = feeAssetType,
permissionType = permissionType,
)
analyticsEventHandler.send(
@ -286,10 +294,9 @@ internal class GiveApprovalModel @Inject constructor(
}
private fun getApprovalAmount(): BigDecimal? {
return if (uiState.value.approveType == ApproveType.LIMITED) {
params.amount.toBigDecimalOrNull()
} else {
null
return when (uiState.value.approveType) {
ApproveType.LIMITED -> params.amount.parseBigDecimalOrNull()
ApproveType.UNLIMITED -> null
}
}
@ -302,7 +309,7 @@ internal class GiveApprovalModel @Inject constructor(
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
?: return GetFeeError.DataError(IllegalStateException("Currency is not a token")).left()
val amount = params.amount.toBigDecimalOrNull()
val amount = params.amount.parseBigDecimalOrNull()
?: return GetFeeError.DataError(IllegalArgumentException("Invalid amount format")).left()
val allowance = getAllowanceInfoUseCase(

View file

@ -2,16 +2,21 @@ package com.tangem.features.approval.impl.model
import arrow.core.right
import com.google.common.truth.Truth.assertThat
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.model.MutableParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.models.AllowanceInfo
import com.tangem.domain.transaction.models.TransactionFeeExtended
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetAllowanceInfoUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
@ -23,6 +28,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.approval.api.GiveApprovalComponent
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
@ -30,6 +36,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.math.BigDecimal
@OptIn(ExperimentalCoroutinesApi::class)
class GiveApprovalModelTest {
@ -48,31 +55,73 @@ class GiveApprovalModelTest {
private val userWalletId = UserWalletId(stringValue = "0123456789ABCDEF")
private val userWallet: UserWallet.Hot = mockk(relaxed = true)
private val cryptoCurrencyStatus: CryptoCurrencyStatus = mockk {
every { currency } returns mockk<CryptoCurrency.Token>(relaxed = true)
private val tokenCurrency: CryptoCurrency.Token = mockk(relaxed = true) {
every { contractAddress } returns "0xContract"
every { network } returns mockk<Network>(relaxed = true)
}
private val params = GiveApprovalComponent.Params(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
amount = "10",
spenderAddress = "0xSpender",
amountFooter = TextReference.EMPTY,
feeFooter = TextReference.EMPTY,
callback = mockk(relaxed = true),
)
private val cryptoCurrencyStatus: CryptoCurrencyStatus = mockk {
every { currency } returns tokenCurrency
}
private val approvalTx: TransactionData.Uncompiled = mockk(relaxed = true)
private val transactionFee: TransactionFee = mockk(relaxed = true)
private val transactionFeeExtended: TransactionFeeExtended = mockk(relaxed = true) {
every { transactionFee } returns this@GiveApprovalModelTest.transactionFee
}
private val getUserWalletUseCase: GetUserWalletUseCase = mockk(relaxed = true)
@BeforeEach
fun setUp() {
every { getUserWalletUseCase.invoke(userWalletId) } returns userWallet.right()
coEvery {
createApprovalTransactionUseCase(
cryptoCurrencyStatus = any(),
userWalletId = any(),
amount = any(),
contractAddress = any(),
spenderAddress = any(),
)
} returns approvalTx.right()
coEvery {
getAllowanceInfoUseCase(
userWalletId = any(),
cryptoCurrency = any(),
spenderAddress = any(),
requiredAmount = any(),
)
} returns AllowanceInfo.Enough(allowance = BigDecimal.ZERO).right()
coEvery {
getFeeUseCase(transactionData = any(), userWallet = any(), network = any())
} returns transactionFee.right()
coEvery {
getFeeForGaslessUseCase(transactionData = any(), userWallet = any(), network = any())
} returns transactionFeeExtended.right()
coEvery {
getFeeForTokenUseCase(transactionData = any(), userWallet = any(), token = any())
} returns transactionFeeExtended.right()
}
private fun createModel(): GiveApprovalModel = GiveApprovalModel(
private fun createParams(amount: String): GiveApprovalComponent.Params = GiveApprovalComponent.Params(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
amount = amount,
spenderAddress = "0xSpender",
amountFooter = TextReference.EMPTY,
feeFooter = TextReference.EMPTY,
callback = mockk(relaxed = true),
)
private fun createModel(amount: String = "10"): GiveApprovalModel = GiveApprovalModel(
dispatchers = TestingCoroutineDispatcherProvider(),
paramsContainer = MutableParamsContainer(params),
paramsContainer = MutableParamsContainer(createParams(amount)),
createApprovalTransactionUseCase = createApprovalTransactionUseCase,
getAllowanceInfoUseCase = getAllowanceInfoUseCase,
sendTransactionUseCase = sendTransactionUseCase,
@ -109,4 +158,118 @@ class GiveApprovalModelTest {
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerLoadingState() }
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerUpdate() }
}
@Test
fun `GIVEN comma decimal amount and LIMITED approveType WHEN loadFeeExtended THEN creates approval tx with parsed amount`() =
runTest {
val model = createModel(amount = "1,1")
val result = model.loadFeeExtended(maybeToken = null)
assertThat(result.isRight()).isTrue()
coVerify {
createApprovalTransactionUseCase(
cryptoCurrencyStatus = any(),
userWalletId = any(),
amount = match { it != null && it.compareTo(BigDecimal("1.1")) == 0 },
contractAddress = any(),
spenderAddress = any(),
)
}
}
@Test
fun `GIVEN point decimal amount and LIMITED approveType WHEN loadFeeExtended THEN creates approval tx with parsed amount`() =
runTest {
val model = createModel(amount = "1.1")
val result = model.loadFeeExtended(maybeToken = null)
assertThat(result.isRight()).isTrue()
coVerify {
createApprovalTransactionUseCase(
cryptoCurrencyStatus = any(),
userWalletId = any(),
amount = match { it != null && it.compareTo(BigDecimal("1.1")) == 0 },
contractAddress = any(),
spenderAddress = any(),
)
}
}
@Test
fun `GIVEN comma decimal amount and UNLIMITED approveType WHEN loadFeeExtended THEN creates approval tx with null amount`() =
runTest {
val model = createModel(amount = "1,1")
model.onChangeApproveType(ApproveType.UNLIMITED)
val result = model.loadFeeExtended(maybeToken = null)
assertThat(result.isRight()).isTrue()
coVerify {
createApprovalTransactionUseCase(
cryptoCurrencyStatus = any(),
userWalletId = any(),
amount = isNull(),
contractAddress = any(),
spenderAddress = any(),
)
}
}
@Test
fun `GIVEN unparseable amount and LIMITED approveType WHEN loadFeeExtended THEN creates approval tx with null amount`() =
runTest {
val model = createModel(amount = "abc")
val result = model.loadFeeExtended(maybeToken = null)
assertThat(result.isRight()).isTrue()
coVerify {
createApprovalTransactionUseCase(
cryptoCurrencyStatus = any(),
userWalletId = any(),
amount = isNull(),
contractAddress = any(),
spenderAddress = any(),
)
}
}
@Test
fun `GIVEN comma decimal amount and LIMITED approveType WHEN loadFee THEN creates approval tx with parsed amount`() =
runTest {
val model = createModel(amount = "2,5")
val result = model.loadFee()
assertThat(result.isRight()).isTrue()
coVerify {
createApprovalTransactionUseCase(
cryptoCurrencyStatus = any(),
userWalletId = any(),
amount = match { it != null && it.compareTo(BigDecimal("2.5")) == 0 },
contractAddress = any(),
spenderAddress = any(),
)
}
}
@Test
fun `GIVEN unparseable amount WHEN loadFee THEN returns DataError and does not check allowance`() = runTest {
val model = createModel(amount = "abc")
val result = model.loadFee()
assertThat(result.isLeft()).isTrue()
assertThat(result.leftOrNull()).isInstanceOf(GetFeeError.DataError::class.java)
coVerify(exactly = 0) {
getAllowanceInfoUseCase(
userWalletId = any(),
cryptoCurrency = any(),
spenderAddress = any(),
requiredAmount = any(),
)
}
}
}

View file

@ -141,12 +141,12 @@ internal class AddToPortfolioModel @Inject constructor(
// but its call AddToPortfolioManager.onAddedTokenClick callback
!isAvailableToAdd -> {
val singleNetwork = portfolio.account.addedMarketNetworks.first()
callbackDelegate.onNetworkSelected(singleNetwork)
callbackDelegate.onNetworkSelected.send(singleNetwork)
}
// force select a network, triggers [selectedNetwork]
isSingleAvailableNetwork -> {
val singleNetwork = portfolio.account.availableToAddNetworks.first()
callbackDelegate.onNetworkSelected(singleNetwork)
callbackDelegate.onNetworkSelected.send(singleNetwork)
}
// it's important to control root screen, UI depends on it(close/arrow icon)
isSinglePortfolio -> navigation.replaceAll(routeToNetworkSelector(portfolio))
@ -358,7 +358,7 @@ internal class AddToPortfolioModel @Inject constructor(
val isSingleAvailableNetwork = portfolio.account.isSingleNetwork
if (isSingleAvailableNetwork) {
val singleNetwork = portfolio.account.availableToAddNetworks.first()
callbackDelegate.onNetworkSelected(singleNetwork)
callbackDelegate.onNetworkSelected.send(singleNetwork)
} else {
navigation.pushNew(routeToNetworkSelector(portfolio))
}

View file

@ -23,6 +23,7 @@ import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDi
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.IntroductionProcess.CreateWalletIntroScreenOpened.ScreenType
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.SaveWalletError
@ -137,6 +138,10 @@ internal class CreateWalletStartModel @Inject constructor(
modelScope.launch {
analyticsEventHandler.send(
event = IntroductionProcess.CreateWalletIntroScreenOpened(
screenType = when (params.mode) {
CreateWalletStartComponent.Mode.ColdWallet -> ScreenType.Cold
CreateWalletStartComponent.Mode.HotWallet -> ScreenType.Hot
},
referralId = appsFlyerStore.get()?.refcode,
),
)

View file

@ -32,14 +32,13 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.domain.models.earn.PreselectedEarnType
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.feed.components.feed.FeedBottomSheetRoute
import kotlinx.serialization.Serializable
import com.tangem.features.feed.model.earn.EarnModel
import com.tangem.features.feed.model.earn.analytics.EarnSource
import com.tangem.features.feed.ui.components.FeedSearchBar
import com.tangem.features.feed.ui.earn.EarnContent
import kotlinx.serialization.Serializable
internal class DefaultEarnComponent(
appComponentContext: AppComponentContext,
@ -116,17 +115,12 @@ internal class DefaultEarnComponent(
componentContext: ComponentContext,
): ComposableBottomSheetComponent = when (config) {
is FeedBottomSheetRoute.AddToPortfolio -> {
val manager = checkNotNull(earnModel.currentAddToPortfolioManager) {
"currentAddToPortfolioManager must be set before activating AddToPortfolio slot"
}
addToPortfolioComponentFactory.create(
context = childByContext(componentContext),
params = AddToPortfolioComponent.Params(
addToPortfolioManager = when {
config.source == EarnSource.BEST_OPPORTUNITIES_SOURCE.value ->
earnModel.addBestOpportunitiesPortfolioManager
config.source == EarnSource.MOSTLY_USED_SOURCE.value ->
earnModel.addMostlyUsedPortfolioManager
else -> error("Unknown source: ${config.source}")
},
),
params = AddToPortfolioComponent.Params(addToPortfolioManager = manager),
)
}
is FeedBottomSheetRoute.NetworkFilter -> EarnNetworkFilterComponent(

View file

@ -95,11 +95,12 @@ internal class DefaultFeedComponent(
componentContext: ComponentContext,
): ComposableBottomSheetComponent = when (config) {
is FeedBottomSheetRoute.AddToPortfolio -> {
val manager = checkNotNull(feedComponentModel.currentAddToPortfolioManager) {
"currentAddToPortfolioManager must be set before activating AddToPortfolio slot"
}
addToPortfolioComponentFactory.create(
context = childByContext(componentContext),
params = AddToPortfolioComponent.Params(
addToPortfolioManager = feedComponentModel.addToPortfolioManager,
),
params = AddToPortfolioComponent.Params(addToPortfolioManager = manager),
)
}
is FeedBottomSheetRoute.NetworkFilter -> EmptyComposableBottomSheetComponent

View file

@ -22,8 +22,9 @@ import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.earn.EarnNetworks
import com.tangem.domain.models.earn.EarnTokenWithCurrency
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.domain.models.earn.PreselectedEarnType
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager.AnalyticsParams.Companion.CategoryEarn
import com.tangem.features.feed.components.earn.DefaultEarnComponent
import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent
import com.tangem.features.feed.components.earn.EarnTypeFilterComponent
@ -44,8 +45,8 @@ import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
import com.tangem.features.feed.ui.earn.state.EarnUM
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ -85,24 +86,13 @@ internal class EarnModel @Inject constructor(
dispatchers = dispatchers,
)
val addBestOpportunitiesPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create(
scope = modelScope,
settings = AddToPortfolioManager.Settings.Earn,
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = EarnSource.BEST_OPPORTUNITIES_SOURCE.value),
).apply {
updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected)
}
val addMostlyUsedPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create(
scope = modelScope,
settings = AddToPortfolioManager.Settings.Earn,
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = EarnSource.MOSTLY_USED_SOURCE.value),
).apply {
updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected)
}
val bottomSheetNavigation: SlotNavigation<FeedBottomSheetRoute> = SlotNavigation()
var currentAddToPortfolioManager: AddToPortfolioManager? = null
private set
private var currentAddToPortfolioManagerScope: CoroutineScope? = null
val state: StateFlow<EarnUM>
get() = stateController.uiState
@ -114,30 +104,6 @@ internal class EarnModel @Inject constructor(
subscribeOnNetworks()
subscribeOnBatchFlow()
subscribeToMostlyUsed()
addBestOpportunitiesPortfolioManager.onDismiss.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.launchIn(modelScope)
addBestOpportunitiesPortfolioManager.onSuccessAdded.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.onEach(::openCurrencyDetails)
.launchIn(modelScope)
addBestOpportunitiesPortfolioManager.onAddedTokenClick.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.onEach(::openCurrencyDetails)
.launchIn(modelScope)
addMostlyUsedPortfolioManager.onDismiss.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.launchIn(modelScope)
addMostlyUsedPortfolioManager.onSuccessAdded.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.onEach(::openCurrencyDetails)
.launchIn(modelScope)
addMostlyUsedPortfolioManager.onAddedTokenClick.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.onEach(::openCurrencyDetails)
.launchIn(modelScope)
}
private fun openCurrencyDetails(result: AddToPortfolioManager.Result) {
@ -354,17 +320,44 @@ internal class EarnModel @Inject constructor(
contractAddress = earnTokenWithCurrency.earnToken.tokenAddress,
decimalCount = earnTokenWithCurrency.earnToken.decimalCount,
)
when (source) {
EarnSource.BEST_OPPORTUNITIES_SOURCE -> addBestOpportunitiesPortfolioManager.apply {
setTokenParams(token)
setTokenNetworks(listOf(network))
}
EarnSource.MOSTLY_USED_SOURCE -> addMostlyUsedPortfolioManager.apply {
setTokenParams(token)
setTokenNetworks(listOf(network))
}
val manager = createAddToPortfolioManager(source = source).apply {
setTokenParams(token)
setTokenNetworks(listOf(network))
}
bottomSheetNavigation.activate(FeedBottomSheetRoute.AddToPortfolio(source.value))
currentAddToPortfolioManager = manager
// Drop the slot through null so the same-source repeat click still recreates the child.
bottomSheetNavigation.dismiss()
bottomSheetNavigation.activate(FeedBottomSheetRoute.AddToPortfolio(source = source.value))
}
private fun createAddToPortfolioManager(source: EarnSource): AddToPortfolioManager {
currentAddToPortfolioManagerScope?.cancel()
val managerScope = CoroutineScope(
modelScope.coroutineContext + SupervisorJob(modelScope.coroutineContext.job),
)
currentAddToPortfolioManagerScope = managerScope
val manager = addToPortfolioManagerFactory.create(
scope = managerScope,
settings = AddToPortfolioManager.Settings.Earn,
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = source.value, category = CategoryEarn),
).apply {
updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected)
}
manager.onDismiss.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.launchIn(managerScope)
manager.onSuccessAdded.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.onEach(::openCurrencyDetails)
.launchIn(managerScope)
manager.onAddedTokenClick.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.onEach(::openCurrencyDetails)
.launchIn(managerScope)
return manager
}
private fun onTypeFilterOptionSelected(type: EarnFilterType) {

View file

@ -41,10 +41,8 @@ import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentHashMap
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import javax.inject.Inject
@ -62,7 +60,7 @@ internal class FeedComponentModel @Inject constructor(
private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase,
private val appRouter: AppRouter,
private val designFeatureToggles: DesignFeatureToggles,
addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
paramsContainer: ParamsContainer,
@ -87,16 +85,13 @@ internal class FeedComponentModel @Inject constructor(
dispatchers = dispatchers,
)
val addToPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create(
scope = modelScope,
settings = AddToPortfolioManager.Settings.Earn,
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = AnalyticsParam.ScreensSources.Markets.value),
).apply {
updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected)
}
val bottomSheetNavigation: SlotNavigation<FeedBottomSheetRoute> = SlotNavigation()
var currentAddToPortfolioManager: AddToPortfolioManager? = null
private set
private var currentAddToPortfolioManagerScope: CoroutineScope? = null
val state: StateFlow<FeedListUM>
get() = stateController.uiState
@ -110,18 +105,6 @@ internal class FeedComponentModel @Inject constructor(
fetchCharts()
subscribeOnCurrencyUpdate()
subscribeOnDataState()
addToPortfolioManager.onDismiss.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.launchIn(modelScope)
addToPortfolioManager.onSuccessAdded.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.onEach(::openCurrencyDetails)
.launchIn(modelScope)
addToPortfolioManager.onAddedTokenClick.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.onEach(::openCurrencyDetails)
.launchIn(modelScope)
}
private fun openCurrencyDetails(result: AddToPortfolioManager.Result) {
@ -431,12 +414,46 @@ internal class FeedComponentModel @Inject constructor(
contractAddress = earnTokenWithCurrency.earnToken.tokenAddress,
decimalCount = earnTokenWithCurrency.earnToken.decimalCount,
)
val route = FeedBottomSheetRoute.AddToPortfolio(AnalyticsParam.ScreensSources.Markets.value)
addToPortfolioManager.apply {
val manager = createAddToPortfolioManager().apply {
setTokenParams(token)
setTokenNetworks(listOf(network))
}
bottomSheetNavigation.activate(route)
currentAddToPortfolioManager = manager
// Drop the slot through null so the same-source repeat click still recreates the child.
bottomSheetNavigation.dismiss()
bottomSheetNavigation.activate(FeedBottomSheetRoute.AddToPortfolio(AnalyticsParam.ScreensSources.Markets.value))
}
private fun createAddToPortfolioManager(): AddToPortfolioManager {
currentAddToPortfolioManagerScope?.cancel()
val managerScope = CoroutineScope(
modelScope.coroutineContext + SupervisorJob(modelScope.coroutineContext.job),
)
currentAddToPortfolioManagerScope = managerScope
val manager = addToPortfolioManagerFactory.create(
scope = managerScope,
settings = AddToPortfolioManager.Settings.Earn,
analyticsParams = AddToPortfolioManager.AnalyticsParams(
source = AnalyticsParam.ScreensSources.Markets.value,
),
).apply {
updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected)
}
manager.onDismiss.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.launchIn(managerScope)
manager.onSuccessAdded.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.onEach(::openCurrencyDetails)
.launchIn(managerScope)
manager.onAddedTokenClick.receiveAsFlow()
.onEach { bottomSheetNavigation.dismiss() }
.onEach(::openCurrencyDetails)
.launchIn(managerScope)
return manager
}
private fun handleEarnPageOpenClicked() {

View file

@ -30,6 +30,7 @@ internal sealed class SendAnalyticEvents(
val isNonceNotEmpty: Boolean,
private val ensStatus: AnalyticsParam.EmptyFull,
private val feeToken: String,
private val feeAssetType: AnalyticsParam.FeeAssetType,
private val fromDerivationIndex: Int?,
private val toDerivationIndex: Int?,
) : SendAnalyticEvents(
@ -47,6 +48,7 @@ internal sealed class SendAnalyticEvents(
}
put(ENS_ADDRESS, ensAddress)
put(FEE_TOKEN, feeToken)
put(AnalyticsParam.Key.FEE_ASSET_TYPE, feeAssetType.value)
},
), AppsFlyerIncludedEvent

View file

@ -31,6 +31,11 @@ internal class SendAnalyticHelper @Inject constructor(
val feeSelectorUM = sendUM.feeSelectorUM as? FeeSelectorUM.Content ?: return
val feeType = feeSelectorUM.toAnalyticType()
val feeTokenSymbol = feeToken.symbol
val feeAssetType = if (feeToken is CryptoCurrency.Coin) {
AnalyticsParam.FeeAssetType.Coin
} else {
AnalyticsParam.FeeAssetType.Token
}
val fromDerivationIndex = account?.derivationIndex?.value
val destination = destinationUM?.addressTextField?.actualAddress ?: return
val destinationAccount = getAccountCurrencyByAddressUseCase(destination)
@ -46,6 +51,7 @@ internal class SendAnalyticHelper @Inject constructor(
fromDerivationIndex = fromDerivationIndex,
toDerivationIndex = toDerivationIndex,
feeToken = feeTokenSymbol,
feeAssetType = feeAssetType,
),
)
analyticsEventHandler.send(
@ -55,6 +61,7 @@ internal class SendAnalyticHelper @Inject constructor(
token = cryptoCurrency.symbol,
feeType = feeType,
feeToken = feeTokenSymbol,
feeAssetType = feeAssetType,
),
memoType = getSendTransactionMemoType(destinationUM.memoTextField),
),

View file

@ -37,6 +37,7 @@ internal class NFTSendAnalyticHelper @Inject constructor(
token = NFT_SEND_CATEGORY, // should send "NFT" in token param
feeType = feeType,
feeToken = cryptoCurrency.symbol,
feeAssetType = AnalyticsParam.FeeAssetType.Coin,
),
memoType = getSendTransactionMemoType(destinationUM?.memoTextField),
),

View file

@ -73,6 +73,7 @@ internal class StakingAnalyticSender(
feeType = AnalyticsParam.FeeType.Normal,
permissionType = ApproveType.LIMITED.name,
feeToken = tokenCryptoCurrency.symbol,
feeAssetType = AnalyticsParam.FeeAssetType.Coin,
),
memoType = Basic.TransactionSent.MemoType.Null,
),
@ -82,7 +83,7 @@ internal class StakingAnalyticSender(
fun sendTransactionStakingAnalytics(value: StakingUiState, cryptoCurrencyStatus: CryptoCurrencyStatus) {
val validatorState = value.validatorState as? StakingStates.ValidatorState.Data
val validatorName = validatorState?.chosenTarget?.name ?: return
val feeAssetType = AnalyticsParam.FeeAssetType.Coin // support only coin as fee asset for staking transactions
analyticsEventHandler.send(
Basic.TransactionSent(
sentFrom = AnalyticsParam.TxSentFrom.Staking(
@ -90,6 +91,7 @@ internal class StakingAnalyticSender(
token = value.cryptoCurrencySymbol,
feeType = AnalyticsParam.FeeType.Normal,
feeToken = cryptoCurrencyStatus.currency.symbol,
feeAssetType = feeAssetType,
),
memoType = Basic.TransactionSent.MemoType.Null,
),
@ -98,6 +100,7 @@ internal class StakingAnalyticSender(
StakingAnalyticsEvent.StakeInProgressScreenOpened(
validator = validatorName,
action = getStakingActionType(value),
feeAssetType = feeAssetType,
),
)
}

View file

@ -61,6 +61,7 @@ internal sealed class SendWithSwapAnalyticEvents(
val feeType: AnalyticsParam.FeeType,
val fromToken: CryptoCurrency,
val toToken: CryptoCurrency,
val feeAssetType: AnalyticsParam.FeeAssetType,
val fromDerivationIndex: Int?,
val toDerivationIndex: Int?,
) : SendWithSwapAnalyticEvents(
@ -74,6 +75,7 @@ internal sealed class SendWithSwapAnalyticEvents(
put(RECEIVE_BLOCKCHAIN, toToken.network.name)
if (fromDerivationIndex != null) put(ACCOUNT_DERIVATION_FROM, fromDerivationIndex.toString())
if (toDerivationIndex != null) put(ACCOUNT_DERIVATION_TO, toDerivationIndex.toString())
put(AnalyticsParam.Key.FEE_ASSET_TYPE, feeAssetType.value)
},
), AppsFlyerIncludedEvent

View file

@ -503,12 +503,19 @@ internal class SendWithSwapConfirmModel @Inject constructor(
.getOrNull()?.account
val toDerivationIndex = destinationAccount?.derivationIndex?.value
val feeToken = getSelectedFeeToken()
val feeAssetType = if (feeToken is CryptoCurrency.Coin) {
AnalyticsParam.FeeAssetType.Coin
} else {
AnalyticsParam.FeeAssetType.Token
}
analyticsEventHandler.send(
SendWithSwapAnalyticEvents.TransactionScreenOpened(
providerName = selectedProvider.name,
feeType = feeType,
fromToken = fromCurrency,
toToken = toCurrency,
feeAssetType = feeAssetType,
fromDerivationIndex = fromDerivationIndex,
toDerivationIndex = toDerivationIndex,
),
@ -519,7 +526,8 @@ internal class SendWithSwapConfirmModel @Inject constructor(
blockchain = fromCurrency.network.name,
token = fromCurrency.symbol,
feeType = feeType,
feeToken = getSelectedFeeToken().symbol,
feeToken = feeToken.symbol,
feeAssetType = feeAssetType,
),
memoType = Basic.TransactionSent.MemoType.Null,
),

View file

@ -1950,6 +1950,7 @@ internal class SwapInteractorImpl @Inject constructor(
private fun Fee.increaseGasLimitBy(percentage: Int): Fee {
if (this !is Fee.Ethereum) return this
val gasLimit = this.gasLimit
if (gasLimit == BigInteger.ZERO) return this
val increasedGasPrice = this.amount.value?.movePointRight(this.amount.decimals)
?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP)
val increasedGasLimit = gasLimit.multiply(percentage.toBigInteger()).divide(hundredPercent)

View file

@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FR
import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_TO
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TOKEN
import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER
import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN
@ -103,6 +104,7 @@ sealed class SwapEvents(
val sendToken: String,
val receiveToken: String,
val feeToken: String,
val feeAssetType: AnalyticsParam.FeeAssetType,
val fromDerivationIndex: Int?,
val toDerivationIndex: Int?,
val referralId: String?,
@ -118,6 +120,7 @@ sealed class SwapEvents(
if (fromDerivationIndex != null) put(ACCOUNT_DERIVATION_FROM, fromDerivationIndex.toString())
if (toDerivationIndex != null) put(ACCOUNT_DERIVATION_TO, toDerivationIndex.toString())
put(FEE_TOKEN, feeToken)
put(AnalyticsParam.Key.FEE_ASSET_TYPE, feeAssetType.value)
putAll(getReferralParams(referralId))
},
), AppsFlyerIncludedEvent

View file

@ -196,6 +196,7 @@ internal class SwapModel @Inject constructor(
isAccountsModeProvider = Provider { isAccountsMode },
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
swapFeatureToggles = swapFeatureToggles,
appRouter = appRouter,
)
private val inputNumberFormatter = InputNumberFormatter(
@ -826,7 +827,7 @@ internal class SwapModel @Inject constructor(
shouldSanitize = false,
)
}
}.getOrNull()
}.getOrNull() ?: fromSwapCurrencyStatus.status
}
dataState = dataState.copy(feePaidCryptoCurrency = feePaidCryptoCurrency)
@ -1180,7 +1181,7 @@ internal class SwapModel @Inject constructor(
}
},
)
sendSuccessEvent()
sendSwapInProgressEvent()
router.replaceAll(SwapRoute.Success)
}
@ -1256,7 +1257,7 @@ internal class SwapModel @Inject constructor(
}
}
private suspend fun sendSuccessEvent() {
private suspend fun sendSwapInProgressEvent() {
val provider = dataState.selectedProvider ?: return
val fee = (getSelectedFee() as? TxFee.Legacy)?.feeType ?: FeeType.NORMAL
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus ?: return
@ -1264,6 +1265,12 @@ internal class SwapModel @Inject constructor(
val fromDerivationIndex = fromSwapCurrencyStatus.account.derivationIndex?.value
val toDerivationIndex = toSwapCurrencyStatus.account.derivationIndex?.value
val feeToken = getFeeToken()
val feeAssetType = if (feeToken is CryptoCurrency.Coin) {
AnalyticsParam.FeeAssetType.Coin
} else {
AnalyticsParam.FeeAssetType.Token
}
analyticsEventHandler.send(
SwapEvents.SwapInProgressScreen(
provider = provider,
@ -1272,7 +1279,8 @@ internal class SwapModel @Inject constructor(
receiveBlockchain = toSwapCurrencyStatus.currency.network.name,
sendToken = fromSwapCurrencyStatus.currency.symbol,
receiveToken = toSwapCurrencyStatus.currency.symbol,
feeToken = getFeeToken().symbol,
feeToken = feeToken.symbol,
feeAssetType = feeAssetType,
fromDerivationIndex = fromDerivationIndex,
toDerivationIndex = toDerivationIndex,
referralId = appsFlyerStore.get()?.refcode,
@ -1599,16 +1607,6 @@ internal class SwapModel @Inject constructor(
onProviderFilterSelect = { filterType ->
uiState = stateBuilder.updateProviderFilterType(uiState, filterType)
},
onBuyClick = {
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus ?: return@UiActions
val feePaidCryptoCurrency = dataState.feePaidCryptoCurrency ?: return@UiActions
val route = AppRoute.CurrencyDetails(
userWalletId = fromSwapCurrencyStatus.userWalletId,
currency = feePaidCryptoCurrency.currency,
)
appRouter.push(route)
},
onRetryClick = {
startLoadingQuotesFromLastState()
},
@ -1678,11 +1676,18 @@ internal class SwapModel @Inject constructor(
}
private fun sendSuccessSwapEvent(fromToken: CryptoCurrency, feeType: FeeType) {
val feeToken = getFeeToken()
val feeAssetType = if (feeToken is CryptoCurrency.Coin) {
AnalyticsParam.FeeAssetType.Coin
} else {
AnalyticsParam.FeeAssetType.Token
}
val event = AnalyticsParam.TxSentFrom.Swap(
blockchain = fromToken.network.name,
token = fromToken.symbol,
feeType = AnalyticsParam.FeeType.fromString(feeType.getNameForAnalytics()),
feeToken = getFeeToken().symbol,
feeToken = feeToken.symbol,
feeAssetType = feeAssetType,
)
analyticsEventHandler.send(
Basic.TransactionSent(

View file

@ -1,6 +1,8 @@
package com.tangem.feature.swap.model
import com.tangem.common.TangemSiteUrlBuilder
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification
@ -106,6 +108,7 @@ internal class SwapNotificationsFactory(
feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
selectedFeeType: FeeType,
hideFee: Boolean,
appRouter: AppRouter,
): ImmutableList<NotificationUM> {
val warnings = buildList {
maybeAddRentExemptionError(quoteModel)
@ -113,7 +116,12 @@ internal class SwapNotificationsFactory(
maybeAddNeedReserveToCreateAccountWarning(quoteModel)
maybeAddPermissionNeededWarning(quoteModel)
maybeAddNetworkFeeCoverageWarning(quoteModel, selectedFeeType)
maybeAddUnableCoverFeeWarning(quoteModel, feeCryptoCurrencyStatus, hideFee)
maybeAddUnableCoverFeeWarning(
quoteModel = quoteModel,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
hideFee = hideFee,
appRouter = appRouter,
)
maybeAddTransactionInProgressWarning(quoteModel)
maybeAddPriceImpactNotification(quoteModel.priceImpact)
}
@ -295,17 +303,20 @@ internal class SwapNotificationsFactory(
}
}
@Suppress("CyclomaticComplexMethod")
private fun MutableList<NotificationUM>.maybeAddUnableCoverFeeWarning(
quoteModel: SwapState.QuotesLoadedState,
feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
hideFee: Boolean,
appRouter: AppRouter,
) {
if (hideFee) return
val fromCurrency = quoteModel.fromTokenInfo.swapCurrencyStatus.currency
if (hideFee || feeCryptoCurrencyStatus == null) return
val fromSwapCurrency = quoteModel.fromTokenInfo.swapCurrencyStatus
val fromCurrency = fromSwapCurrency.currency
val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough
val shouldShowCoverWarning = !quoteModel.preparedSwapConfigState.isBalanceEnough &&
quoteModel.permissionState !is PermissionDataState.PermissionLoading &&
feeCryptoCurrencyStatus?.currency != fromCurrency
feeCryptoCurrencyStatus.currency != fromCurrency
val isCEXProvider = quoteModel.swapProvider.type == ExchangeProviderType.CEX
@ -315,13 +326,25 @@ internal class SwapNotificationsFactory(
val isGaslessAvailable = isGaslessFeeSupportedForNetwork(fromCurrency.network) && isCEXProvider
if (shouldShowCoverWarning && !isGaslessAvailable || isNotEnoughFee) {
add(
SwapNotificationUM.Error.UnableToCoverFeeWarning(
fromToken = fromCurrency,
feeCurrency = feeCryptoCurrencyStatus?.currency,
currencyName = feeEnoughState?.currencyName ?: fromCurrency.network.name,
currencySymbol = feeEnoughState?.currencySymbol ?: fromCurrency.network.currencySymbol,
onConfirmClick = actions.onBuyClick,
),
if (fromCurrency.id == feeCryptoCurrencyStatus.currency.id) {
SwapNotificationUM.Error.InsufficientFunds
} else {
val route = AppRoute.CurrencyDetails(
userWalletId = fromSwapCurrency.userWalletId,
currency = feeCryptoCurrencyStatus.currency,
)
SwapNotificationUM.Error.UnableToCoverFeeWarning(
fromToken = fromCurrency,
feeCurrency = feeCryptoCurrencyStatus.currency,
currencyName = feeEnoughState?.currencyName ?: fromCurrency.network.name,
currencySymbol = feeEnoughState?.currencySymbol ?: fromCurrency.network.currencySymbol,
onConfirmClick = if (!appRouter.stack.contains(route)) {
{ appRouter.push(route) }
} else {
null
},
)
},
)
}
}

View file

@ -24,7 +24,6 @@ internal data class UiActions(
val onProviderClick: (String) -> Unit,
val onProviderSelect: (String) -> Unit,
val onProviderFilterSelect: (ProviderFilterType) -> Unit,
val onBuyClick: () -> Unit,
val onSelectTokenClick: (TokenSelectionDirection) -> Unit,
val onSuccess: () -> Unit,
val onLinkClick: (String) -> Unit,

View file

@ -58,12 +58,17 @@ internal object SwapNotificationUM {
),
)
data object InsufficientFunds : Error(
title = resourceReference(R.string.swapping_insufficient_funds),
subtitle = resourceReference(R.string.swapping_insufficient_funds_description),
)
data class UnableToCoverFeeWarning(
val fromToken: CryptoCurrency,
val currencyName: String,
val currencySymbol: String,
val feeCurrency: CryptoCurrency?,
val onConfirmClick: () -> Unit,
val feeCurrency: CryptoCurrency,
val onConfirmClick: (() -> Unit)?,
) : Error(
title = resourceReference(
R.string.warning_express_not_enough_fee_for_token_tx_title,
@ -74,7 +79,7 @@ internal object SwapNotificationUM {
wrappedList(currencyName, currencySymbol),
),
iconResId = fromToken.networkIconResId,
buttonState = feeCurrency?.let {
buttonState = onConfirmClick?.let {
NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_buy_currency, wrappedList(currencySymbol)),
onClick = onConfirmClick,

View file

@ -3,6 +3,7 @@ package com.tangem.feature.swap.ui
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
@ -11,13 +12,16 @@ import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToI
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.express.models.ProviderFilterType
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.parseBigDecimalOrNull
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ProviderFilterType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -27,20 +31,16 @@ import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNet
import com.tangem.feature.swap.converters.SwapProviderStateBuilder
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount
import com.tangem.feature.swap.domain.models.domain.RateType
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.model.SwapNotificationsFactory
import com.tangem.feature.swap.model.SwapProcessDataState
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.SwapButton.Mode
import com.tangem.feature.swap.models.states.*
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.feature.swap.presentation.R
import com.tangem.feature.swap.utils.formatToUIRepresentation
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -54,7 +54,7 @@ import java.math.BigDecimal
/**
* State builder creates a specific states for SwapScreen
*/
@Suppress("LargeClass", "TooManyFunctions")
@Suppress("LargeClass", "TooManyFunctions", "LongParameterList")
internal class StateBuilder(
private val actions: UiActions,
private val isBalanceHiddenProvider: Provider<Boolean>,
@ -62,6 +62,7 @@ internal class StateBuilder(
private val isAccountsModeProvider: Provider<Boolean>,
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
private val swapFeatureToggles: SwapFeatureToggles,
private val appRouter: AppRouter,
) {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
@ -469,6 +470,7 @@ internal class StateBuilder(
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
selectedFeeType = selectedFeeType,
hideFee = hideFee,
appRouter = appRouter,
)
val fromAccountTitleUM = when {

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap
import com.google.common.truth.Truth.assertThat
import com.tangem.common.routing.AppRouter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.models.account.Account
@ -33,6 +34,7 @@ internal class StateBuilderInitialStateTest {
private val isAccountsModeProvider: Provider<Boolean> = mockk()
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
private val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true)
private val appRouter: AppRouter = mockk()
private lateinit var sut: StateBuilder
@ -51,6 +53,7 @@ internal class StateBuilderInitialStateTest {
isAccountsModeProvider = isAccountsModeProvider,
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
swapFeatureToggles = swapFeatureToggles,
appRouter = appRouter
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap
import com.google.common.truth.Truth.assertThat
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.wallet.UserWallet
@ -28,6 +29,7 @@ internal class StateBuilderPairsTest {
private val isAccountsModeProvider: Provider<Boolean> = mockk()
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
private val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true)
private val appRouter: AppRouter = mockk()
private lateinit var sut: StateBuilder
@ -56,6 +58,7 @@ internal class StateBuilderPairsTest {
isAccountsModeProvider = isAccountsModeProvider,
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
swapFeatureToggles = swapFeatureToggles,
appRouter = appRouter,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap
import com.google.common.truth.Truth.assertThat
import com.tangem.common.routing.AppRouter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -31,6 +32,7 @@ internal class StateBuilderQuotesTest {
private val isAccountsModeProvider: Provider<Boolean> = mockk()
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
private val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true)
private val appRouter: AppRouter = mockk()
private lateinit var sut: StateBuilder
@ -60,6 +62,7 @@ internal class StateBuilderQuotesTest {
isAccountsModeProvider = isAccountsModeProvider,
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
swapFeatureToggles = swapFeatureToggles,
appRouter = appRouter,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap
import com.google.common.truth.Truth.assertThat
import com.tangem.common.routing.AppRouter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -33,6 +34,7 @@ internal class StateBuilderSwapDataTest {
private val isAccountsModeProvider: Provider<Boolean> = mockk()
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
private val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true)
private val appRouter: AppRouter = mockk()
private lateinit var sut: StateBuilder
@ -62,6 +64,7 @@ internal class StateBuilderSwapDataTest {
isAccountsModeProvider = isAccountsModeProvider,
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
swapFeatureToggles = swapFeatureToggles,
appRouter = appRouter,
)
}

View file

@ -58,9 +58,7 @@ internal class TangemPayCardPageScreenComponent(
TangemPayCardPageScreen(
state = state,
cardDetailsBlockComponent = cardDetailsBlockComponent,
cardDetailsState = cardDetailsState.copy(
isActive = !state.isReissueInProgress,
),
cardDetailsState = cardDetailsState,
modifier = modifier,
)
bottomSheet.child?.instance?.BottomSheet()

View file

@ -41,6 +41,7 @@ internal class TangemPayEditDisplayNameComponent(
displayNameState = DisplayNameState.Editing(
displayName = state.editingValue.text,
editingValue = state.editingValue,
isSubmitEnabled = state.isDoneEnabled,
onValueChanged = state.onValueChanged,
onSubmit = state.onDoneClick,
onDismiss = state.onDismiss,

View file

@ -7,7 +7,6 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo
import com.tangem.features.tangempay.model.TangemPayReissueCardModel
import com.tangem.features.tangempay.ui.TangemPayReissueCardContent
@ -34,7 +33,6 @@ internal class TangemPayReissueCardComponent(
}
internal interface ReissueCardListener {
fun onReissueOrderCreate(order: TangemPayReissueOrderInfo)
fun onDismissReissueCard()
fun onClickAddFunds()
}

View file

@ -21,7 +21,11 @@ internal class TangemPayDetailsStateFactory(
private val cardFrozenState: TangemPayCardFrozenState,
) {
@Suppress("LongMethod")
fun getInitialState(isTangemPayDeactivated: Boolean, cardNumberEnd: String): TangemPayDetailsUM {
fun getInitialState(
isTangemPayDeactivated: Boolean,
cardNumberEnd: String,
isReissuing: Boolean,
): TangemPayDetailsUM {
return TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(
onBackClick = onBack,
@ -51,7 +55,8 @@ internal class TangemPayDetailsStateFactory(
cards = persistentListOf(
TangemPayDetailsBalanceBlockState.Card(
lastDigits = cardNumberEnd,
onClick = intents::onCardClick,
onClick = {},
isReissuing = isReissuing,
),
),
onAddCardClick = intents::onAddCardClick,

View file

@ -31,7 +31,7 @@ internal data class TangemPayCardDetailsUM(
val isLoading: Boolean = false,
val cardFrozenState: TangemPayCardFrozenState,
val displayNameState: DisplayNameState?,
val isActive: Boolean = true,
val isActionsAvailable: Boolean = false,
)
internal sealed interface DisplayNameState {
@ -47,6 +47,7 @@ internal sealed interface DisplayNameState {
data class Editing(
override val displayName: String,
val editingValue: TextFieldValue,
val isSubmitEnabled: Boolean,
val onValueChanged: (TextFieldValue) -> Unit,
val onSubmit: () -> Unit,
val onDismiss: () -> Unit,
@ -83,7 +84,11 @@ internal sealed class TangemPayDetailsBalanceBlockState {
) : TangemPayDetailsBalanceBlockState()
data class CardsBlockState(val cards: ImmutableList<Card>, val onAddCardClick: () -> Unit)
data class Card(val lastDigits: String, val onClick: () -> Unit)
data class Card(
val lastDigits: String,
val onClick: () -> Unit,
val isReissuing: Boolean,
)
}
internal data class AddToWalletBlockState(

View file

@ -5,6 +5,7 @@ import androidx.compose.ui.text.input.TextFieldValue
internal data class TangemPayEditDisplayNameUM(
val editingValue: TextFieldValue,
val isLoading: Boolean,
val isDoneEnabled: Boolean,
val onValueChanged: (TextFieldValue) -> Unit,
val onDoneClick: () -> Unit,
val onDismiss: () -> Unit,

View file

@ -75,7 +75,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
private val showCardDetailsTimerJobHolder = JobHolder()
init {
subscribeToCardNameChanges(cardId = card.id, userWalletId = params.userWalletId)
subscribeToCardChanges(cardId = card.id, userWalletId = params.userWalletId)
subscribeToCardFrozenState()
modelScope.launch {
cardDetailsEventListener.event.collectLatest { event ->
@ -87,7 +87,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
}
}
private fun subscribeToCardNameChanges(cardId: String, userWalletId: UserWalletId) {
private fun subscribeToCardChanges(cardId: String, userWalletId: UserWalletId) {
paymentAccountStatusSupplier.invoke(userWalletId)
.onEach { state ->
val status = state.value
@ -98,7 +98,11 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
val card = status.requireCardWithId(cardId)
val displayName = card.displayName ?: return@onEach
if (card.isReissuing) {
hideCardDetails()
}
uiState.update(TangemPayCardDetailsUpdateNameTransformer(displayName))
uiState.update { it.copy(isActionsAvailable = !card.isReissuing) }
}
}
.launchIn(modelScope)
@ -142,10 +146,8 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
}
fun hideCardDetails() {
modelScope.launch {
revealCardDetailsJobHolder.cancel()
uiState.transformerUpdate(transformer = DetailsHiddenStateTransformer(stateFactory))
}
revealCardDetailsJobHolder.cancel()
uiState.transformerUpdate(transformer = DetailsHiddenStateTransformer(stateFactory))
}
private fun showError() {

View file

@ -19,6 +19,7 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
import com.tangem.core.ui.format.bigdecimal.optionalDecimals
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.PaymentAccountStatusValue
@ -27,13 +28,10 @@ import com.tangem.domain.models.account.requireCardWithId
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.pay.usecase.ChangeCardFrozenStateUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.features.tangempay.components.AddFundsListener
import com.tangem.features.tangempay.components.ReissueCardListener
import com.tangem.features.tangempay.components.TangemPayCardPageComponent
@ -65,7 +63,7 @@ internal class TangemPayCardPageModel @Inject constructor(
private val analytics: AnalyticsEventHandler,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val uiMessageSender: UiMessageSender,
private val reissueCardRepository: TangemPayReissueCardRepository,
private val changeCardFrozenStateUseCase: ChangeCardFrozenStateUseCase,
) : Model(), ViewPinListener, ReissueCardListener, AddFundsListener {
private val params: TangemPayCardPageComponent.Params = paramsContainer.require()
@ -75,6 +73,7 @@ internal class TangemPayCardPageModel @Inject constructor(
private val addToWalletBannerJobHolder = JobHolder()
private val addFundsJobHolder = JobHolder()
private val frozenStateJobHolder = JobHolder()
val uiState: StateFlow<TangemPayCardPageUM>
field = MutableStateFlow(
@ -87,7 +86,6 @@ internal class TangemPayCardPageModel @Inject constructor(
val bottomSheetNavigation: SlotNavigation<TangemPayCardNavigation> = SlotNavigation()
// TODO v_rodionov: #[REDACTED_TASK_KEY] check reissue order state before card details are showed
init {
analytics.send(TangemPayAnalyticsEvents.CardManagementScreenOpened())
fetchAddToWalletBanner()
@ -112,7 +110,13 @@ internal class TangemPayCardPageModel @Inject constructor(
} else {
TangemPayDailyLimitBlockState.Error
}
uiState.update { it.copy(dailyLimitState = dailyLimitState, settings = buildSettings(card)) }
uiState.update { uiState ->
uiState.copy(
dailyLimitState = dailyLimitState,
settings = buildSettings(card),
isReissueInProgress = card.isReissuing,
)
}
} else {
uiState.update { it.copy(dailyLimitState = TangemPayDailyLimitBlockState.Error) }
}
@ -125,12 +129,18 @@ internal class TangemPayCardPageModel @Inject constructor(
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_change_pin),
onSettingClick = { onClickChangePIN(card.hasPinCode) },
testTag = com.tangem.core.ui.test.TangemPayTestTags.CHANGE_PIN_ROW,
testTag = TangemPayTestTags.CHANGE_PIN_ROW,
),
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_freeze_card),
title = TextReference.Res(
if (card.isFrozen) {
R.string.tangempay_card_details_unfreeze_card
} else {
R.string.tangempay_card_details_freeze_card
},
),
onSettingClick = { onClickFreezeOrUnfreezeCard(card.isFrozen) },
testTag = com.tangem.core.ui.test.TangemPayTestTags.FREEZE_CARD_ROW,
testTag = TangemPayTestTags.FREEZE_CARD_ROW,
),
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_reissue_card),
@ -158,6 +168,8 @@ internal class TangemPayCardPageModel @Inject constructor(
}
private fun onClickFreezeOrUnfreezeCard(isFrozen: Boolean) {
if (frozenStateJobHolder.isActive) return
val message = if (isFrozen) {
TangemPayMessagesFactory.createUnfreezeCardMessage(onUnfreezeClicked = ::unfreezeCard)
} else {
@ -171,18 +183,6 @@ internal class TangemPayCardPageModel @Inject constructor(
bottomSheetNavigation.activate(TangemPayCardNavigation.ReissueCard)
}
override fun onReissueOrderCreate(order: TangemPayReissueOrderInfo) {
bottomSheetNavigation.dismiss()
onReissueOrderStatusReceived(order.orderStatus)
if (order.orderStatus != OrderStatus.CANCELED) {
modelScope.launch {
reissueCardRepository.storeReissueOrderId(cardId, order.orderId)
}
} else {
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.common_something_went_wrong)))
}
}
override fun onDismissReissueCard() {
bottomSheetNavigation.dismiss()
}
@ -244,40 +244,34 @@ internal class TangemPayCardPageModel @Inject constructor(
private fun freezeCard() {
modelScope.launch {
cardDetailsRepository.freezeCard(
changeCardFrozenStateUseCase(
userWalletId = userWalletId,
cardId = cardId,
isFreezing = true,
).onLeft {
val message = SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_failed))
uiMessageSender.send(message)
}.onRight { state ->
val message = if (state == TangemPayCardFrozenState.Frozen) {
SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_success))
} else {
SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_failed))
}
}.onRight {
val message = SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_success))
uiMessageSender.send(message)
}
}
}.saveIn(frozenStateJobHolder)
}
private fun unfreezeCard() {
modelScope.launch {
cardDetailsRepository.unfreezeCard(
changeCardFrozenStateUseCase(
userWalletId = userWalletId,
cardId = cardId,
isFreezing = false,
).onLeft {
val message = SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_failed))
uiMessageSender.send(message)
}.onRight { state ->
val message = if (state == TangemPayCardFrozenState.Unfrozen) {
SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_success))
} else {
SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_failed))
}
}.onRight {
val message = SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_success))
uiMessageSender.send(message)
}
}
}.saveIn(frozenStateJobHolder)
}
private fun fetchAddToWalletBanner() {
@ -315,18 +309,4 @@ internal class TangemPayCardPageModel @Inject constructor(
override fun onDismissViewPin() {
bottomSheetNavigation.dismiss()
}
private fun onReissueOrderStatusReceived(orderStatus: OrderStatus) {
when (orderStatus) {
OrderStatus.NEW, OrderStatus.PROCESSING, OrderStatus.COMPLETED -> {
uiState.update { state ->
state.copy(
addToWalletBlockState = null,
isReissueInProgress = true,
)
}
}
OrderStatus.CANCELED -> Unit
}
}
}

View file

@ -19,9 +19,11 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
@ -49,10 +51,7 @@ import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.logging.TangemLogger
import com.tangem.utils.transformer.update
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -61,6 +60,7 @@ import javax.inject.Inject
@ModelScoped
internal class TangemPayDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
override val dispatchers: CoroutineDispatcherProvider,
private val analytics: AnalyticsEventHandler,
private val router: Router,
@ -102,6 +102,7 @@ internal class TangemPayDetailsModel @Inject constructor(
stateFactory.getInitialState(
isTangemPayDeactivated = isTangemPayDeactivated,
cardNumberEnd = firstCard?.lastDigits.orEmpty(),
isReissuing = firstCard?.isReissuing ?: false,
),
)
@ -120,6 +121,21 @@ internal class TangemPayDetailsModel @Inject constructor(
if (!isTangemPayDeactivated && firstCard != null) {
subscribeToCardFrozenState(firstCard.id)
fetchAddToWalletBanner()
paymentAccountStatusSupplier.invoke(userWalletId)
.map { it.value }
.filterIsInstance<PaymentAccountStatusValue.Loaded>()
.filter { it.source == StatusSource.ACTUAL }
.onEach { state ->
val card = state.cards.firstOrNull() ?: return@onEach
uiState.update(
TangemPayCardDataTransformer(
card = card,
onCardClick = { onCardClick() },
),
)
}
.launchIn(modelScope)
}
}

View file

@ -52,6 +52,7 @@ internal class TangemPayEditDisplayNameModel @Inject constructor(
selection = TextRange(originalDisplayName.length),
),
isLoading = false,
isDoneEnabled = true,
onValueChanged = ::onValueChanged,
onDoneClick = ::onDoneClick,
onDismiss = ::onDismiss,
@ -87,9 +88,9 @@ internal class TangemPayEditDisplayNameModel @Inject constructor(
}
private fun onValueChanged(value: TextFieldValue) {
if (value.text.length <= CardDisplayName.MAX_LENGTH) {
uiState.update { it.copy(editingValue = value) }
}
val displayName = CardDisplayName(value.text)
val isAvailableForConfirm = displayName.isRight()
uiState.update { it.copy(editingValue = value, isDoneEnabled = isAvailableForConfirm) }
}
private fun onDoneClick() {

View file

@ -13,6 +13,7 @@ import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
import com.tangem.domain.pay.usecase.ReissueTangemPayCardUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.features.tangempay.components.TangemPayReissueCardComponent
import com.tangem.features.tangempay.details.impl.R
@ -29,6 +30,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class TangemPayReissueCardModel @Inject constructor(
@ -36,6 +38,7 @@ internal class TangemPayReissueCardModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val reissueCardRepository: TangemPayReissueCardRepository,
private val reissueTangemPayCardUseCase: ReissueTangemPayCardUseCase,
private val uiMessageSender: UiMessageSender,
private val analytics: AnalyticsEventHandler,
) : Model() {
@ -72,15 +75,13 @@ internal class TangemPayReissueCardModel @Inject constructor(
analytics.send(TangemPayAnalyticsEvents.ReplaceCardConfirmed())
state.update { it.copy(isReissuingInProgress = true) }
modelScope.launch {
reissueCardRepository.reissueCard(
reissueTangemPayCardUseCase(
userWalletId = params.userWalletId,
cardId = params.cardId,
).onLeft {
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.common_something_went_wrong)))
onDismiss()
}.onRight { order ->
params.listener.onReissueOrderCreate(order)
}
onDismiss()
}.saveIn(reissueJobHolder)
}
@ -96,7 +97,7 @@ internal class TangemPayReissueCardModel @Inject constructor(
val error = if (fee == null || cardBalance == null) {
TangemPayReissueCardError.InitialDataLoading
} else if (cardBalance.availableForWithdrawal < fee.amount) {
} else if (cardBalance.fiatBalance < fee.amount) {
TangemPayReissueCardError.InsufficientFunds
} else {
null

View file

@ -0,0 +1,30 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
internal class TangemPayCardDataTransformer(
private val card: TangemPayCard,
private val onCardClick: () -> Unit,
) : Transformer<TangemPayDetailsUM> {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
val updatedCard = TangemPayDetailsBalanceBlockState.Card(
lastDigits = card.lastDigits,
onClick = onCardClick,
isReissuing = card.isReissuing,
)
val cardsBlockState = prevState.balanceBlockState.cardsBlockState?.copy(
cards = persistentListOf(updatedCard),
)
val newBalanceBlockState = when (val bs = prevState.balanceBlockState) {
is TangemPayDetailsBalanceBlockState.Loading -> bs.copy(cardsBlockState = cardsBlockState)
is TangemPayDetailsBalanceBlockState.Content -> bs.copy(cardsBlockState = cardsBlockState)
is TangemPayDetailsBalanceBlockState.Error -> bs.copy(cardsBlockState = cardsBlockState)
}
return prevState.copy(balanceBlockState = newBalanceBlockState)
}
}

View file

@ -50,7 +50,6 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.DisplayNameState
@ -98,7 +97,7 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M
shape = RoundedCornerShape(16.dp),
),
) {
if (shouldShowDetails && state.isActive) {
if (shouldShowDetails) {
TangemPayCardDetailsShownBlock(
cardNumber = state.number,
expiry = state.expiry,
@ -148,7 +147,7 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
)
}
if (state.isActive) {
if (state.isActionsAvailable) {
ConstraintLayout(
modifier = Modifier
.align(Alignment.BottomCenter)
@ -273,11 +272,7 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo
BasicTextField(
value = state.editingValue,
onValueChange = { newValue ->
if (newValue.text.length <= CardDisplayName.MAX_LENGTH) {
state.onValueChanged(newValue)
}
},
onValueChange = state.onValueChanged,
modifier = modifier
.width(textWidthDp.coerceAtLeast(1.dp))
.focusRequester(focusRequester),
@ -285,7 +280,13 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo
singleLine = true,
cursorBrush = SolidColor(TangemTheme.colors.text.constantWhite),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { state.onSubmit() }),
keyboardActions = KeyboardActions(
onDone = if (state.isSubmitEnabled) {
{ state.onSubmit() }
} else {
null
},
),
decorationBox = { innerTextField ->
Box {
if (state.editingValue.text.isEmpty()) {
@ -481,6 +482,7 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
displayName = "Tangem",
editingValue = TextFieldValue(text = "movet", selection = TextRange("movet".length)),
onValueChanged = {},
isSubmitEnabled = true,
onSubmit = {},
onDismiss = {},
),
@ -500,6 +502,7 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
displayName = "Tangem",
editingValue = TextFieldValue(text = ""),
onValueChanged = {},
isSubmitEnabled = true,
onSubmit = {},
onDismiss = {},
),

View file

@ -24,10 +24,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -76,23 +73,25 @@ internal fun TangemPayCardPageScreen(
state = cardDetailsState,
)
}
if (state.addToWalletBlockState != null) {
cardPageItem(key = "GooglePay") {
TangemPayAddToWalletBlock(state = state.addToWalletBlockState)
}
}
cardPageItem(key = "Limit") {
TangemPayDailyLimitBlock(state = state.dailyLimitState)
}
if (state.dailyLimitState == TangemPayDailyLimitBlockState.Error) {
cardPageItem(key = "LimitError") {
TangemPayDailyLimitErrorBlock()
}
}
cardPageItem(key = "Settings") {
if (state.isReissueInProgress) {
if (state.isReissueInProgress) {
cardPageItem(key = "Reissue") {
TangemPayReplacingCardBlock()
} else {
}
} else {
if (state.addToWalletBlockState != null) {
cardPageItem(key = "GooglePay") {
TangemPayAddToWalletBlock(state = state.addToWalletBlockState)
}
}
cardPageItem(key = "Limit") {
TangemPayDailyLimitBlock(state = state.dailyLimitState)
}
if (state.dailyLimitState == TangemPayDailyLimitBlockState.Error) {
cardPageItem(key = "LimitError") {
TangemPayDailyLimitErrorBlock()
}
}
cardPageItem(key = "Settings") {
TangemPayCardPageSettingsBlock(settings = state.settings)
}
}
@ -100,19 +99,6 @@ internal fun TangemPayCardPageScreen(
}
}
@Composable
private fun TangemPayReplacingCardBlock(modifier: Modifier = Modifier) {
Notification(
modifier = modifier,
config = NotificationConfig(
iconResId = com.tangem.core.ui.R.drawable.ic_update_32,
iconTint = NotificationConfig.IconTint.Accent,
title = resourceReference(R.string.tangempay_reissue_card_in_progress),
subtitle = resourceReference(R.string.tangempay_reissue_card_in_progress_description),
),
)
}
@Composable
private fun TangemPayCardPageSettingsBlock(
settings: ImmutableList<TangemPayCardPageSetting>,

View file

@ -31,6 +31,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.RectangleShimmer
@ -106,6 +107,7 @@ internal fun TangemPayDetailsScreen(
TangemPayDetailsBalanceBlock(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(top = 12.dp)
.fillMaxWidth(),
state = state.balanceBlockState,
isBalanceHidden = state.isBalanceHidden,
@ -113,30 +115,45 @@ internal fun TangemPayDetailsScreen(
SpacerH12()
},
)
if (state.addToWalletBlockState != null) {
if (state.balanceBlockState.cardsBlockState?.cards?.fastAny { it.isReissuing } == true) {
item(
key = AddToWalletBlockState::class.java,
key = "REISSUE_MESSAGE",
content = {
TangemPayAddToWalletBlock(
state = state.addToWalletBlockState,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
)
},
)
}
if (state.accountDeactivatedNotificationConfig != null) {
item(
key = "DEACTIVATION_MESSAGE",
content = {
Notification(
modifier = modifier
TangemPayReplacingCardBlock(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
config = state.accountDeactivatedNotificationConfig,
)
SpacerH12()
},
)
} else {
if (state.addToWalletBlockState != null) {
item(
key = AddToWalletBlockState::class.java,
content = {
TangemPayAddToWalletBlock(
state = state.addToWalletBlockState,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
)
},
)
}
if (state.accountDeactivatedNotificationConfig != null) {
item(
key = "DEACTIVATION_MESSAGE",
content = {
Notification(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
config = state.accountDeactivatedNotificationConfig,
)
SpacerH12()
},
)
}
}
if (state.accountDeactivatedNotificationConfig == null) {
with(expressTransactionsComponent) {
@ -287,17 +304,25 @@ private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modi
) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(R.drawable.img_visa_card_48x32),
painter = painterResource(
if (card.isReissuing) {
R.drawable.img_visa_card_inactive_48_32
} else {
R.drawable.img_visa_card_48_32
},
),
contentDescription = null,
)
Text(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(4.dp, bottom = 2.dp),
text = card.lastDigits,
style = TangemTheme.typography.overline.copy(letterSpacing = 0.sp),
color = TangemTheme.colors.text.constantWhite,
)
if (!card.isReissuing) {
Text(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(4.dp, bottom = 2.dp),
text = card.lastDigits,
style = TangemTheme.typography.overline.copy(letterSpacing = 0.sp),
color = TangemTheme.colors.text.constantWhite,
)
}
}
}
@ -422,8 +447,8 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
isBalanceFlickering = false,
cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState(
cards = persistentListOf(
TangemPayDetailsBalanceBlockState.Card(lastDigits = "1234", onClick = {}),
TangemPayDetailsBalanceBlockState.Card(lastDigits = "3456", onClick = {}),
TangemPayDetailsBalanceBlockState.Card(lastDigits = "1234", onClick = {}, isReissuing = false),
TangemPayDetailsBalanceBlockState.Card(lastDigits = "3456", onClick = {}, isReissuing = false),
),
onAddCardClick = {},
),
@ -439,7 +464,13 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(
actionButtons = persistentListOf(),
cardsBlockState = TangemPayDetailsBalanceBlockState.CardsBlockState(
cards = persistentListOf(TangemPayDetailsBalanceBlockState.Card(lastDigits = "1234", onClick = {})),
cards = persistentListOf(
TangemPayDetailsBalanceBlockState.Card(
lastDigits = "1234",
onClick = {},
isReissuing = true,
),
),
onAddCardClick = {},
),
),

View file

@ -1,25 +1,15 @@
package com.tangem.features.tangempay.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
import com.tangem.features.tangempay.details.impl.R
@ -66,17 +56,15 @@ internal fun TangemPayEditDisplayNameScreen(
Spacer(modifier = Modifier.weight(1f))
NavigationPrimaryButton(
PrimaryButton(
modifier = Modifier
.imePadding()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
primaryButton = NavigationButton(
textReference = resourceReference(R.string.common_done),
onClick = state.onDoneClick,
shouldShowProgress = state.isLoading,
isEnabled = !state.isLoading && state.editingValue.text.isNotBlank(),
),
text = stringResourceSafe(R.string.common_done),
onClick = state.onDoneClick,
showProgress = state.isLoading,
enabled = !state.isLoading && state.isDoneEnabled,
)
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tangempay.details.impl.R
@Composable
internal fun TangemPayReplacingCardBlock(modifier: Modifier = Modifier) {
Notification(
modifier = modifier,
config = NotificationConfig(
iconResId = R.drawable.ic_update_32,
iconTint = NotificationConfig.IconTint.Accent,
title = resourceReference(R.string.tangempay_reissue_card_in_progress),
subtitle = resourceReference(R.string.tangempay_reissue_card_in_progress_description),
),
containerColor = TangemTheme.colors.background.primary,
)
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
TangemPayReplacingCardBlock()
}
}

View file

@ -48,6 +48,7 @@ internal class TangemPayCardLimitSetupModelTest {
isFrozen = false,
lastDigits = "1234",
limit = null,
isReissuing = false,
)
private val initialStatus: AccountStatus.Payment = AccountStatus.Payment(
@ -77,6 +78,7 @@ internal class TangemPayCardLimitSetupModelTest {
)
}
),
isReissuing = false,
)
val statusWithLimit: PaymentAccountStatusValue.Loaded = mockk(relaxed = true) {
every { source } returns StatusSource.ACTUAL

View file

@ -68,7 +68,7 @@ internal class WalletsUpdateActionResolver @Inject constructor(
isAnyWalletUnlocked(state, wallets) -> {
Action.UnlockWallet(
selectedWallet = selectedWallet,
unlockedWallets = wallets.filterNot(UserWallet::isLocked),
unlockedWallets = getJustUnlockedWallets(state = state, wallets = wallets),
)
}
isAnyWalletNameChanged(state, wallets) -> {
@ -232,11 +232,28 @@ internal class WalletsUpdateActionResolver @Inject constructor(
private fun isAnyWalletUnlocked(state: WalletScreenState, wallets: List<UserWallet>): Boolean {
return state.wallets.any { walletState ->
val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } ?: return@any false
!wallet.isLocked &&
(walletState is WalletState.MultiCurrency.Locked || walletState is WalletState.SingleCurrency.Locked)
wallet.isJustUnlockedFrom(walletState)
}
}
/**
* Wallets that transitioned from a locked UI state to unlocked data state in this update cycle.
* Does not include wallets that were already unlocked before otherwise downstream handlers
* (e.g. [com.tangem.feature.wallet.child.wallet.model.WalletModel.unlockWallet]) would re-fetch
* already-loaded wallets and emit redundant transformer state changes.
*/
private fun getJustUnlockedWallets(state: WalletScreenState, wallets: List<UserWallet>): List<UserWallet> {
return state.wallets.mapNotNull { walletState ->
val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id }
wallet?.takeIf { it.isJustUnlockedFrom(walletState) }
}
}
private fun UserWallet.isJustUnlockedFrom(walletState: WalletState): Boolean {
return !isLocked &&
(walletState is WalletState.MultiCurrency.Locked || walletState is WalletState.SingleCurrency.Locked)
}
private fun isSelectedWalletCardsCountChanged(state: WalletScreenState, selectedWallet: UserWallet): Boolean {
if (selectedWallet !is UserWallet.Cold) return false
val prevSelectedWallet = state.getPrevSelectedWallet()

View file

@ -8,12 +8,13 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletScreenStateTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletStateTransformer
import com.tangem.utils.extensions.indexOfFirstOrNull
import com.tangem.utils.logging.TangemLogger
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
import javax.inject.Singleton
@ -41,7 +42,14 @@ internal class WalletStateController @Inject constructor(
}
fun update(transformer: WalletScreenStateTransformer) {
TangemLogger.d("Applying: ${transformer::class.simpleName}")
val walletStateTransformer = transformer as? WalletStateTransformer
val maybeWalletId = if (walletStateTransformer != null) {
" for ${walletStateTransformer.userWalletId}"
} else {
""
}
TangemLogger.d("Applying: ${transformer::class.simpleName}$maybeWalletId")
mutableUiState.update(function = transformer::transform)
}

View file

@ -67,7 +67,7 @@ internal class UnlockWalletTransformer(
is WalletState.MultiCurrency.Content,
is WalletState.SingleCurrency.Content,
-> {
TangemLogger.e("Impossible to unlock wallet with not locked state")
TangemLogger.e("createLoadingState: Impossible to unlock wallet with not locked state")
prevState
}
}
@ -79,7 +79,7 @@ internal class UnlockWalletTransformer(
userWallet = unlockedWallet,
)
is WalletUM.Content -> {
TangemLogger.e("Impossible to unlock wallet with not locked state")
TangemLogger.e("createLoadingState2: Impossible to unlock wallet with not locked state")
walletUM
}
}

View file

@ -7,7 +7,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import kotlinx.collections.immutable.toImmutableList
internal abstract class WalletStateTransformer(
protected val userWalletId: UserWalletId,
internal val userWalletId: UserWalletId,
) : WalletScreenStateTransformer {
abstract fun transform(prevState: WalletState): WalletState

View file

@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert
import androidx.compose.ui.text.SpanStyle
import com.tangem.common.ui.R
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.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
@ -66,7 +67,11 @@ internal class TangemPayMainBlockConverter(
is PaymentAccountStatusValue.Loaded -> {
val card = statusValue.cards.firstOrNull() ?: return TangemPayMainUM.TemporaryUnavailable
TangemPayMainUM.Content(
subtitle = stringReference("*${card.lastDigits}"),
subtitle = if (card.isReissuing) {
resourceReference(R.string.tangempay_status_replacing)
} else {
stringReference("*${card.lastDigits}")
},
isBalanceFlickering = statusValue.source == StatusSource.CACHE,
balance = getBalanceText(
currencyCode = statusValue.currencyCode,

View file

@ -14,6 +14,7 @@ import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.utils.coroutines.combine7
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -22,6 +23,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import java.math.BigDecimal
/**
@ -46,38 +49,69 @@ internal class AccountListSubscriber @AssistedInject constructor(
override val isAddAndManageTokensEnabled: Boolean
get() = walletFeatureToggles.isAddAndManageTokensEnabled
override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7(
flow1 = getAccountStatusListFlow(),
flow2 = getAppCurrencyFlow(),
flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet),
flow4 = accountDependencies.isAccountsModeEnabledUseCase(),
flow5 = yieldSupplyApyFlow(),
flow6 = yieldSupplyGetShouldShowMainPromoFlow(),
flow7 = stakingAvailabilityFlow(),
) {
accountList, appCurrency, expandedAccounts, isAccountMode,
yieldSupplyApyMap, shouldShowMainPromo, stakingAvailabilityMap,
->
if (designFeatureToggles.isRedesignEnabled) {
updateState2(
accountList = accountList,
appCurrency = appCurrency,
expandedAccounts = expandedAccounts,
isAccountMode = isAccountMode,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
)
} else {
updateState(
accountList = accountList,
appCurrency = appCurrency,
expandedAccounts = expandedAccounts,
isAccountMode = isAccountMode,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
override fun create(coroutineScope: CoroutineScope): Flow<*> {
val walletId = userWallet.walletId.stringValue
TangemLogger.i("$TAG[$walletId]: create() called, building combine7")
return combine7(
flow1 = getAccountStatusListFlow()
.onStart { TangemLogger.i("$TAG[$walletId]: flow1 accountStatusList subscribed") }
.onEach { list ->
val count = list.flattenCurrencies().size
TangemLogger.i("$TAG[$walletId]: flow1 accountStatusList emitted (currencies=$count)")
},
flow2 = getAppCurrencyFlow()
.onStart { TangemLogger.i("$TAG[$walletId]: flow2 appCurrency subscribed") }
.onEach { TangemLogger.i("$TAG[$walletId]: flow2 appCurrency emitted=${it.code}") },
flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet)
.onStart { TangemLogger.i("$TAG[$walletId]: flow3 expandedAccounts subscribed") }
.onEach { TangemLogger.i("$TAG[$walletId]: flow3 expandedAccounts emitted (size=${it.size})") },
flow4 = accountDependencies.isAccountsModeEnabledUseCase()
.onStart { TangemLogger.i("$TAG[$walletId]: flow4 isAccountsModeEnabled subscribed") }
.onEach { TangemLogger.i("$TAG[$walletId]: flow4 isAccountsModeEnabled emitted=$it") },
flow5 = yieldSupplyApyFlow()
.onStart { TangemLogger.i("$TAG[$walletId]: flow5 yieldSupplyApy subscribed") }
.onEach { TangemLogger.i("$TAG[$walletId]: flow5 yieldSupplyApy emitted (size=${it.size})") },
flow6 = yieldSupplyGetShouldShowMainPromoFlow()
.onStart { TangemLogger.i("$TAG[$walletId]: flow6 shouldShowMainPromo subscribed") }
.onEach { TangemLogger.i("$TAG[$walletId]: flow6 shouldShowMainPromo emitted=$it") },
flow7 = stakingAvailabilityFlow()
.onStart { TangemLogger.i("$TAG[$walletId]: flow7 stakingAvailability subscribed") }
.onEach { TangemLogger.i("$TAG[$walletId]: flow7 stakingAvailability emitted (size=${it.size})") },
) {
accountList, appCurrency, expandedAccounts, isAccountMode,
yieldSupplyApyMap, shouldShowMainPromo, stakingAvailabilityMap,
->
TangemLogger.i(
"$TAG[$walletId]: combine7 transform fired — " +
"currencies=${accountList.flattenCurrencies().size}, " +
"appCurrency=${appCurrency.code}, " +
"expanded=${expandedAccounts.size}, " +
"isAccountMode=$isAccountMode, " +
"apyMap=${yieldSupplyApyMap.size}, " +
"promo=$shouldShowMainPromo, " +
"stakingMap=${stakingAvailabilityMap.size}",
)
if (designFeatureToggles.isRedesignEnabled) {
updateState2(
accountList = accountList,
appCurrency = appCurrency,
expandedAccounts = expandedAccounts,
isAccountMode = isAccountMode,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
)
} else {
updateState(
accountList = accountList,
appCurrency = appCurrency,
expandedAccounts = expandedAccounts,
isAccountMode = isAccountMode,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
)
}
}
}
@ -104,4 +138,8 @@ internal class AccountListSubscriber @AssistedInject constructor(
interface Factory {
fun create(userWallet: UserWallet): AccountListSubscriber
}
private companion object {
const val TAG = "AccountListSubscriber"
}
}

View file

@ -25,6 +25,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetContractAddressUseCase
@ -141,59 +142,69 @@ internal class YieldSupplyApproveModel @Inject constructor(
userWallet = userWallet,
network = cryptoCurrency.network,
).fold(
ifLeft = { error ->
TangemLogger.e(error.toString())
uiState.update(YieldSupplyTransactionReadyTransformer)
analyticsEventHandler.send(
YieldSupplyAnalytics.EarnErrors(
action = YieldSupplyAnalytics.Action.Approve,
errorDescription = error.getAnalyticsDescription(),
),
)
yieldSupplyAlertFactory.getSendTransactionErrorState(
error = error,
popBack = params.callback::onDismissClick,
onFailedTxEmailClick = { errorMessage ->
modelScope.launch(dispatchers.default) {
yieldSupplyAlertFactory.onFailedTxEmailClick(
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
errorMessage = errorMessage,
)
}
},
)
params.callback.onTransactionProgress(false)
},
ifRight = { txHash ->
yieldSupplyPendingTracker.addPending(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
txIds = listOf(txHash),
)
val event = AnalyticsParam.TxSentFrom.Earning(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = AnalyticsParam.FeeType.Normal,
feeToken = feeCryptoCurrencyStatus.currency.symbol,
)
analyticsEventHandler.send(
Basic.TransactionSent(
sentFrom = event,
memoType = Basic.TransactionSent.MemoType.Null,
),
)
analyticsEventHandler.send(YieldSupplyAnalytics.ApprovalAction(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
action = YieldSupplyAnalytics.Action.Approve,
))
params.callback.onTransactionSent()
},
ifLeft = ::onTransactionError,
ifRight = { onTransactionSuccess(it) },
)
}
}
private fun onTransactionError(error: SendTransactionError) {
TangemLogger.e(error.toString())
uiState.update(YieldSupplyTransactionReadyTransformer)
analyticsEventHandler.send(
YieldSupplyAnalytics.EarnErrors(
action = YieldSupplyAnalytics.Action.Approve,
errorDescription = error.getAnalyticsDescription(),
),
)
yieldSupplyAlertFactory.getSendTransactionErrorState(
error = error,
popBack = params.callback::onDismissClick,
onFailedTxEmailClick = { errorMessage ->
modelScope.launch(dispatchers.default) {
yieldSupplyAlertFactory.onFailedTxEmailClick(
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
errorMessage = errorMessage,
)
}
},
)
params.callback.onTransactionProgress(false)
}
private suspend fun onTransactionSuccess(txHash: String) {
yieldSupplyPendingTracker.addPending(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
txIds = listOf(txHash),
)
val feeAssetType = if (feeCryptoCurrencyStatus.currency is CryptoCurrency.Coin) {
AnalyticsParam.FeeAssetType.Coin
} else {
AnalyticsParam.FeeAssetType.Token
}
val event = AnalyticsParam.TxSentFrom.Earning(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = AnalyticsParam.FeeType.Normal,
feeToken = feeCryptoCurrencyStatus.currency.symbol,
feeAssetType = feeAssetType,
)
analyticsEventHandler.send(
Basic.TransactionSent(
sentFrom = event,
memoType = Basic.TransactionSent.MemoType.Null,
),
)
analyticsEventHandler.send(YieldSupplyAnalytics.ApprovalAction(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
action = YieldSupplyAnalytics.Action.Approve,
))
params.callback.onTransactionSent()
}
private fun subscribeOnCurrencyStatusUpdates() {
modelScope.launch {
feeCryptoCurrencyStatusFlow.update {

Some files were not shown because too many files have changed in this diff Show more