Updated on 2026-08-14
This commit is contained in:
commit
3e70beab28
103 changed files with 1908 additions and 626 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 498b0bcd0d871ed60c43b5d44f646548a4f11d37
|
||||
Subproject commit 158fbd8808d2db92ef82d3f9ed92c81340c707c5
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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 & 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 & 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 networks
and thousands of tokens 0% fee on stablecoin-to-stablecoin swaps</string>
|
||||
<string name="swap_story_third_subtitle_v2">Swap across major networks
and 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>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 772 B After Width: | Height: | Size: 772 B |
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -83,5 +83,6 @@ data class CustomerInfo(
|
|||
val isPinSet: Boolean,
|
||||
val fiatBalance: PaymentAccountStatusValue.FiatBalance,
|
||||
val cryptoBalance: PaymentAccountStatusValue.CryptoBalance,
|
||||
val availableForWithdrawal: BigDecimal,
|
||||
)
|
||||
}
|
||||
|
|
@ -5,4 +5,7 @@ enum class OrderStatus {
|
|||
PROCESSING,
|
||||
COMPLETED,
|
||||
CANCELED,
|
||||
}
|
||||
}
|
||||
|
||||
val OrderStatus.isFinalStatus
|
||||
get() = this == OrderStatus.COMPLETED || this == OrderStatus.CANCELED
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.domain.pay.model
|
||||
|
||||
data class TangemPayReissueOrderInfo(
|
||||
data class TangemPayOrderInfo(
|
||||
val orderId: String,
|
||||
val orderStatus: OrderStatus,
|
||||
)
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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?>
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue