Updated on 2026-08-14
This commit is contained in:
commit
28f550e633
44 changed files with 846 additions and 196 deletions
|
|
@ -433,6 +433,14 @@
|
|||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- SurveySparrow SDK exports this activity (exported="true") in its own manifest. -->
|
||||
<!-- We launch it only via an explicit in-app intent, so force it non-exported to -->
|
||||
<!-- prevent any other installed app from loading arbitrary web content into it. -->
|
||||
<activity
|
||||
android:name="com.surveysparrow.ss_android_sdk.SsSurveyActivity"
|
||||
android:exported="false"
|
||||
tools:replace="android:exported" />
|
||||
</application>
|
||||
<queries>
|
||||
<!-- Needed from Android 11 to open Google Wallet for payment with Visa card -->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.offramp.model.PendingOfframp
|
||||
import com.tangem.domain.offramp.model.PendingOfframp.Companion.EXPIRY_MS
|
||||
import com.tangem.domain.offramp.repository.OfframpRepository
|
||||
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||
import com.tangem.tap.data.converter.PendingOfframpEntryConverter
|
||||
|
|
@ -59,7 +60,7 @@ internal class DefaultOfframpRepository(
|
|||
requestId
|
||||
}
|
||||
|
||||
override suspend fun consumePendingOfframp(
|
||||
override suspend fun resolvePendingOfframp(
|
||||
requestId: String,
|
||||
userWalletId: UserWalletId,
|
||||
currencyId: String,
|
||||
|
|
@ -73,9 +74,9 @@ internal class DefaultOfframpRepository(
|
|||
entry.currencyId == currencyId &&
|
||||
!entry.isExpired(now)
|
||||
}
|
||||
// Remove only the fully-matched record (single-use); always prune expired ones. A request_id that
|
||||
// matches but with a mismatched wallet/currency is left intact so a tampered redirect cannot burn it.
|
||||
stored.filter { it != matched }.filterNotExpired(now)
|
||||
// Keep the matched record so the same redirect can be followed again until it expires; only prune the
|
||||
// expired ones. The record is dropped naturally once it ages past EXPIRY_MS.
|
||||
stored.filterNotExpired(now)
|
||||
}
|
||||
matched?.let(converter::convert)
|
||||
}
|
||||
|
|
@ -84,8 +85,10 @@ internal class DefaultOfframpRepository(
|
|||
pendingOfframpStore.data.first().map(converter::convert)
|
||||
}
|
||||
|
||||
// Returns the same instance when nothing is expired, so DataStore.updateData sees an unchanged value and skips
|
||||
// both the extra allocation and the write.
|
||||
private fun List<PendingOfframpEntry>.filterNotExpired(now: Long): List<PendingOfframpEntry> =
|
||||
filterNot { it.isExpired(now) }
|
||||
if (none { now - it.createdAt >= EXPIRY_MS }) this else filter { now - it.createdAt < EXPIRY_MS }
|
||||
|
||||
private fun PendingOfframpEntry.isExpired(now: Long): Boolean = converter.convert(this).isExpired(now)
|
||||
}
|
||||
|
|
@ -156,13 +156,13 @@ internal class DefaultOfframpRepositoryTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN registered pending offramp WHEN consume with matching wallet and currency THEN returns record`() =
|
||||
fun `GIVEN registered pending offramp WHEN resolve with matching wallet and currency THEN returns record`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val storedRequestId = repository.registerPendingOfframp(userWalletId, currencyId)
|
||||
|
||||
// Act
|
||||
val pending = repository.consumePendingOfframp(storedRequestId, userWalletId, currencyId)
|
||||
val pending = repository.resolvePendingOfframp(storedRequestId, userWalletId, currencyId)
|
||||
|
||||
// Assert
|
||||
assertThat(pending).isNotNull()
|
||||
|
|
@ -172,35 +172,36 @@ internal class DefaultOfframpRepositoryTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unknown request id WHEN consume THEN returns null`() = runTest {
|
||||
fun `GIVEN unknown request id WHEN resolve THEN returns null`() = runTest {
|
||||
repository.registerPendingOfframp(userWalletId, currencyId)
|
||||
|
||||
assertThat(repository.consumePendingOfframp("unknown", userWalletId, currencyId)).isNull()
|
||||
assertThat(repository.resolvePendingOfframp("unknown", userWalletId, currencyId)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN already consumed pending offramp WHEN consume again THEN returns null`() = runTest {
|
||||
fun `GIVEN already resolved pending offramp WHEN resolve again THEN still returns record`() = runTest {
|
||||
// Arrange
|
||||
val storedRequestId = repository.registerPendingOfframp(userWalletId, currencyId)
|
||||
|
||||
// Act
|
||||
val first = repository.consumePendingOfframp(storedRequestId, userWalletId, currencyId)
|
||||
val second = repository.consumePendingOfframp(storedRequestId, userWalletId, currencyId)
|
||||
// Act — resolving does NOT consume the record; the same redirect may be followed again until it expires
|
||||
val first = repository.resolvePendingOfframp(storedRequestId, userWalletId, currencyId)
|
||||
val second = repository.resolvePendingOfframp(storedRequestId, userWalletId, currencyId)
|
||||
|
||||
// Assert
|
||||
assertThat(first).isNotNull()
|
||||
assertThat(second).isNull()
|
||||
assertThat(second).isNotNull()
|
||||
assertThat(second?.requestId).isEqualTo(storedRequestId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN mismatched currency WHEN consume THEN returns null and does NOT burn the pending sell`() = runTest {
|
||||
fun `GIVEN mismatched currency WHEN resolve THEN returns null and leaves the pending sell resolvable`() = runTest {
|
||||
// Arrange
|
||||
val storedRequestId = repository.registerPendingOfframp(userWalletId, currencyId)
|
||||
|
||||
// Act — a tampered redirect with the right request_id but a wrong currency must not consume the token
|
||||
val mismatched = repository.consumePendingOfframp(storedRequestId, userWalletId, currencyId = "ethereum")
|
||||
// ...so the legitimate redirect can still succeed afterwards
|
||||
val legitimate = repository.consumePendingOfframp(storedRequestId, userWalletId, currencyId)
|
||||
// Act — a tampered redirect with the right request_id but a wrong currency must not match
|
||||
val mismatched = repository.resolvePendingOfframp(storedRequestId, userWalletId, currencyId = "ethereum")
|
||||
// ...and the legitimate redirect must still resolve afterwards
|
||||
val legitimate = repository.resolvePendingOfframp(storedRequestId, userWalletId, currencyId)
|
||||
|
||||
// Assert
|
||||
assertThat(mismatched).isNull()
|
||||
|
|
@ -209,16 +210,16 @@ internal class DefaultOfframpRepositoryTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN mismatched wallet WHEN consume THEN returns null`() = runTest {
|
||||
fun `GIVEN mismatched wallet WHEN resolve THEN returns null`() = runTest {
|
||||
val storedRequestId = repository.registerPendingOfframp(userWalletId, currencyId)
|
||||
|
||||
val result = repository.consumePendingOfframp(storedRequestId, UserWalletId("ffeeddccbbaa9988"), currencyId)
|
||||
val result = repository.resolvePendingOfframp(storedRequestId, UserWalletId("ffeeddccbbaa9988"), currencyId)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN expired pending offramp WHEN consume THEN returns null`() = runTest {
|
||||
fun `GIVEN expired pending offramp WHEN resolve THEN returns null`() = runTest {
|
||||
// Arrange — seed a record created 2 hours ago (past the 1h expiry)
|
||||
val expiredId = "expired-id"
|
||||
pendingStoreState.value = listOf(
|
||||
|
|
@ -231,7 +232,7 @@ internal class DefaultOfframpRepositoryTest {
|
|||
)
|
||||
|
||||
// Act
|
||||
val pending = repository.consumePendingOfframp(expiredId, userWalletId, currencyId)
|
||||
val pending = repository.resolvePendingOfframp(expiredId, userWalletId, currencyId)
|
||||
|
||||
// Assert
|
||||
assertThat(pending).isNull()
|
||||
|
|
@ -245,7 +246,7 @@ internal class DefaultOfframpRepositoryTest {
|
|||
// Act
|
||||
val stored = repository.getAllStoredOfframps()
|
||||
// ...the record must survive the read so it can still be consumed afterwards
|
||||
val consumed = repository.consumePendingOfframp(storedRequestId, userWalletId, currencyId)
|
||||
val consumed = repository.resolvePendingOfframp(storedRequestId, userWalletId, currencyId)
|
||||
|
||||
// Assert
|
||||
assertThat(stored).hasSize(1)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import androidx.compose.runtime.ReadOnlyComposable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.BlurredEdgeTreatment
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.innerShadow
|
||||
import androidx.compose.ui.platform.testTag
|
||||
|
|
@ -35,6 +34,7 @@ import com.tangem.common.ui.earn.EarnBlockUM.Type
|
|||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.haze.hazeForegroundEffectTangem
|
||||
import com.tangem.core.ui.ds.button.*
|
||||
import com.tangem.core.ui.ds.image.TangemIcon
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
|
|
@ -44,6 +44,8 @@ import com.tangem.core.ui.extensions.*
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
|
||||
import dev.chrisbanes.haze.HazeStyle
|
||||
import dev.chrisbanes.haze.HazeTint
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
private const val TINTED_BACKGROUND_ALPHA = 0.1f
|
||||
|
|
@ -365,7 +367,14 @@ private fun EarnBlockIcon(type: Type, iconUM: EarnBlockUM.IconUM, modifier: Modi
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens2.x6)
|
||||
.blur(radius = TangemTheme.dimens2.x4, edgeTreatment = BlurredEdgeTreatment.Unbounded)
|
||||
.hazeForegroundEffectTangem(
|
||||
style = HazeStyle(
|
||||
tint = HazeTint(Color.Transparent),
|
||||
blurRadius = TangemTheme.dimens2.x4,
|
||||
),
|
||||
) {
|
||||
blurredEdgeTreatment = BlurredEdgeTreatment.Unbounded
|
||||
}
|
||||
.background(color = type.accentGlow().copy(alpha = GLOW_ALPHA), shape = glowShape),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@
|
|||
<string name="common_action_failed">%s fehlgeschlagen</string>
|
||||
<string name="common_activate">Aktivieren</string>
|
||||
<string name="common_add">Hinzufügen</string>
|
||||
<string name="common_add_funds">Guthaben hinzufügen</string>
|
||||
<string name="common_add_funds">Einzahlen</string>
|
||||
<string name="common_add_to_portfolio">Zum Portfolio hinzufügen</string>
|
||||
<string name="common_add_token">Token hinzufügen</string>
|
||||
<string name="common_add_tokens">Token hinzufügen</string>
|
||||
|
|
@ -725,6 +725,11 @@
|
|||
<string name="feedback_subject_tx_failed">Eine Transaktion kann nicht gesendet werden</string>
|
||||
<string name="feedback_token_description_error">Fehler in der Coinbeschreibung</string>
|
||||
<string name="for_you_description">Portfolio prüfen und Verdienstmöglichkeiten erkunden</string>
|
||||
<string name="for_you_earn_opportunities">Verdienstmöglichkeiten</string>
|
||||
<string name="for_you_earn_opportunities_all_tokens_active">Alle Ihre Ressourcen sind im Einsatz – entdecken Sie neue Möglichkeiten</string>
|
||||
<string name="for_you_earn_opportunities_no_available_tokens">Erhalten Sie jährlich bis zu %1$s</string>
|
||||
<string name="for_you_earn_opportunities_tokens_rewards">Maximale potenzielle Gewinne %1$s</string>
|
||||
<string name="for_you_earn_per_year">%1$s/Jahr</string>
|
||||
<string name="for_you_portfolio_review_title">Portfolio-Überprüfung</string>
|
||||
<string name="for_you_title">Für dich</string>
|
||||
<string name="force_update_action">Jetzt aktualisieren</string>
|
||||
|
|
@ -923,20 +928,22 @@
|
|||
<string name="markets_insights_info_description_message">Die Daten dieses Abschnitts stammen aus den folgenden Netzwerken: %s</string>
|
||||
<string name="markets_loading_error_title">Die Daten konnten nicht geladen werden...</string>
|
||||
<string name="markets_loading_no_data_title">Keine Daten</string>
|
||||
<string name="markets_portfolio_block_add_token_title">**Hinzufügen zu Ihrem Portfolio**, um mit dem Kauf, Tausch oder Erhalt dieses Vermögenswerts zu beginnen</string>
|
||||
<string name="markets_portfolio_block_subtitle">Zum Portfolio hinzufügen</string>
|
||||
<string name="markets_portfolio_block_add_token_title">**Füge dieses Produkt Deinem Portfolio hinzu**, um es zu kaufen, zu tauschen oder zu erhalten.</string>
|
||||
<string name="markets_portfolio_block_subtitle">In Deinem Portfolio</string>
|
||||
<string name="markets_portfolio_block_title">Dein Portfolio</string>
|
||||
<string name="markets_portfolio_block_token_unsupported">**Token nicht unterstützt**. Dieser Token wird derzeit in der Wallet nicht unterstützt.</string>
|
||||
<string name="markets_portfolio_eligible_block_title">Andere zulässige Token</string>
|
||||
<string name="markets_pulse_common_title">Marktimpuls</string>
|
||||
<string name="markets_quick_actions">Schnelle Aktionen</string>
|
||||
<string name="markets_search_clear_all_hints">Alles löschen</string>
|
||||
<string name="markets_search_header_title">Token suchen</string>
|
||||
<string name="markets_search_hint_header">Neueste</string>
|
||||
<string name="markets_search_hint_header">Letzte Suchanfragen</string>
|
||||
<string name="markets_search_portfolio_header">In Ihrem Portfolio</string>
|
||||
<string name="markets_search_recent_tokens_header">Aktuelle Token</string>
|
||||
<string name="markets_search_result_title">Ergebnis</string>
|
||||
<string name="markets_search_see_tokens_under_100k">Token unter 100k USD Marktkapitalisierung anzeigen</string>
|
||||
<string name="markets_search_show_tokens">Token anzeigen</string>
|
||||
<string name="markets_search_title_placeholder">Krypto, Nachrichten und mehr</string>
|
||||
<string name="markets_search_title_placeholder">Krypto, Neuigkeiten und mehr</string>
|
||||
<string name="markets_search_token_no_result_title">Kein Ergebnis</string>
|
||||
<string name="markets_select_network">Netzwerk auswählen</string>
|
||||
<string name="markets_select_wallet">Wallet auswählen</string>
|
||||
|
|
@ -1010,7 +1017,7 @@
|
|||
<string name="markets_token_details_market_rating_description">Position im Krypto-Rating zwischen allen Coins basierend auf der Marktkapitalisierung</string>
|
||||
<string name="markets_token_details_market_rating_full">Marktposition</string>
|
||||
<string name="markets_token_details_max_supply">Maximale Versorgung</string>
|
||||
<string name="markets_token_details_max_supply_and_circulation_full">Zirkulation und maximale Versorgung</string>
|
||||
<string name="markets_token_details_max_supply_and_circulation_full">Umlauf- und Maximalversorgung</string>
|
||||
<string name="markets_token_details_max_supply_description">Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können</string>
|
||||
<string name="markets_token_details_max_supply_full">Maximale Versorgung</string>
|
||||
<string name="markets_token_details_metrics">Metriken</string>
|
||||
|
|
@ -1028,7 +1035,7 @@
|
|||
<string name="markets_token_details_trading_volume">Handelsvolumen (24h)</string>
|
||||
<string name="markets_token_details_trading_volume_24h_description">Der Gesamtbetrag einer Kryptowährung, der innerhalb der letzten 24 Stunden gehandelt wurde, wobei das Aktivitäts- und Liquiditätsniveau auf dem Markt angegeben wird</string>
|
||||
<string name="markets_token_details_trading_volume_full">Handelsvolumen (24h)</string>
|
||||
<string name="markets_token_details_valuation_value_in_total">%s insgesamt</string>
|
||||
<string name="markets_token_details_valuation_value_in_total">%s in Summe</string>
|
||||
<string name="markets_token_details_volume">Volumen</string>
|
||||
<string name="markets_tooltip_message">Rufe dies auf oder tippe auf die Suchleiste, um Token direkt vom Markt hinzuzufügen</string>
|
||||
<string name="markets_tooltip_title">Token hinzufügen</string>
|
||||
|
|
@ -1352,10 +1359,10 @@
|
|||
<string name="qr_scanner_warning_unknown_parameters_title">Unbekannte Parameter</string>
|
||||
<string name="quick_action_buy_description">Kreditkarte oder Bankkonto</string>
|
||||
<string name="quick_action_receive_description">Teilen deine Adresse oder dein QR-Code</string>
|
||||
<string name="quick_action_sell_description">Sicherer Verkauf von Kryptowährungen</string>
|
||||
<string name="quick_action_sell_description">Kryptowährung sicher verkaufen</string>
|
||||
<string name="quick_action_send_and_swap_description">Senden Sie mit Tausch an ein anderes Token</string>
|
||||
<string name="quick_action_send_description">An eine andere Wallet senden</string>
|
||||
<string name="quick_action_swap_description">Zwischen deinen Portfolios</string>
|
||||
<string name="quick_action_swap_description">Krypto gegen Krypto</string>
|
||||
<string name="quick_top_up_chip_other">Andere</string>
|
||||
<string name="quick_top_up_title">Schnell aufladen</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">Kein Memo erforderlich</string>
|
||||
|
|
@ -1917,6 +1924,10 @@
|
|||
<item quantity="one">%d Karte</item>
|
||||
<item quantity="other">%d Karten</item>
|
||||
</plurals>
|
||||
<string name="tangempay_cashback_deactivated_description">Dies geschah aufgrund Ihres verdächtigen Verhaltens. Wenden Sie sich an den Support, um mehr zu erfahren.</string>
|
||||
<string name="tangempay_cashback_deactivated_title">Cashback deaktiviert</string>
|
||||
<string name="tangempay_cashback_deposited_on">Wird eingezahlt am %1$s</string>
|
||||
<string name="tangempay_cashback_widget_title">%1$s Cashback in %2$s</string>
|
||||
<string name="tangempay_change_pin_code">PIN-Code ändern</string>
|
||||
<string name="tangempay_come_back_if_forget_pin">Kehren Sie zur App zurück, falls Sie ihn vergessen.</string>
|
||||
<string name="tangempay_common_card">Karte</string>
|
||||
|
|
@ -1980,6 +1991,7 @@
|
|||
<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_main_select_plan">Tarif auswählen</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_newonboard_Q1_body">Ja — für die Nutzung einer regulierten Visa-Karte ist eine Identitätsprüfung Pflicht. Das KYC wird von Sumsub abgewickelt, dem Compliance-Partner.</string>
|
||||
|
|
@ -2043,6 +2055,7 @@
|
|||
<string name="tangempay_set_pin_header">Neue PIN einrichten</string>
|
||||
<string name="tangempay_set_pin_title">PIN einstellen</string>
|
||||
<string name="tangempay_status_deactivated">Konto geschlossen</string>
|
||||
<string name="tangempay_status_inactive">Inaktiv</string>
|
||||
<string name="tangempay_status_replacing">Ersetzen deine Karte</string>
|
||||
<string name="tangempay_sync_needed">Karte oder Ring verwenden, um die Sitzung zu verlängern</string>
|
||||
<string name="tangempay_sync_needed_body">Karte oder Ring verwenden, um die Sitzung zu verlängern</string>
|
||||
|
|
@ -2058,6 +2071,8 @@
|
|||
<string name="tangempay_topup_swap_body">Laden Sie Ihr Konto mit einem beliebigen Token aus Ihrer Wallet auf</string>
|
||||
<string name="tangempay_topup_swap_title">Aus Ihrer Tangem Wallet</string>
|
||||
<string name="tangempay_usdc_on_polygon_network">USDC im Polygon</string>
|
||||
<string name="tangempay_va_banking_details_error_description">Bitte versuche es später erneut. Sollte das Problem weiterhin bestehen, kontaktiere bitte unseren Support. Wir helfen Dir gerne bei der Lösung.</string>
|
||||
<string name="tangempay_va_banking_details_error_title">Die Bankdaten konnten nicht geladen werden</string>
|
||||
<string name="tangempay_visa_benefits">Visavorteile</string>
|
||||
<string name="tangempay_withdrawal_note_description">Gelder aus erstatteten Käufen werden nicht auf Ihr On-Chain-Guthaben zurückerstattet und stehen nicht für Abhebungen zur Verfügung, bleiben aber auf Ihrem Kartenguthaben für Einkäufe verfügbar</string>
|
||||
<string name="tangempay_withdrawal_note_title">Bitte beachten Sie</string>
|
||||
|
|
@ -2622,7 +2637,7 @@
|
|||
<string name="yield_module_approve_sheet_fee_note">Die Gebühr wird abgezogen und Dein Vermögen wird erneut verliehen.</string>
|
||||
<string name="yield_module_approve_sheet_subtitle">Um weiterhin Geld verdienen zu können, ist eine Genehmigung erforderlich.</string>
|
||||
<string name="yield_module_approve_sheet_title">Genehmigung bestätigen</string>
|
||||
<string name="yield_module_average_apy">Durchschnittlicher Jahreszins %1$s%%</string>
|
||||
<string name="yield_module_average_apy">Aktueller effektiver Jahreszins %1$s%%</string>
|
||||
<string name="yield_module_balance_info_sheet_subtitle">Deine Gelder werden derzeit dem Aave-Protokoll bereitgestellt, Du kannst sie jedoch jederzeit verwalten.</string>
|
||||
<string name="yield_module_balance_info_sheet_title">Deine%s ist in Aave hinterlegt</string>
|
||||
<string name="yield_module_chart_loading_error">Chart konnte nicht geladen werden...</string>
|
||||
|
|
|
|||
|
|
@ -709,6 +709,7 @@
|
|||
<string name="generic_error">Ocurrió un error</string>
|
||||
<string name="generic_error_code">Ocurrió un error. Código: %s</string>
|
||||
<string name="generic_requires_memo_error">Requiere memo</string>
|
||||
<string name="get_token_title">Obtener %1$s</string>
|
||||
<string name="give_permission_current_transaction">Esta transacción</string>
|
||||
<string name="give_permission_policy_type_footer">Al aprobar, permite que el contrato inteligente utilice sus tokens en transacciones futuras.</string>
|
||||
<string name="give_permission_rows_amount">Montante %s</string>
|
||||
|
|
@ -725,6 +726,8 @@
|
|||
<string name="hardware_wallet_key_feature_title">Generación de claves</string>
|
||||
<string name="hardware_wallet_security_feature_description">Todas las operaciones criptográficas ocurren dentro del chip seguro, certificado contra la clonación y la manipulación física.</string>
|
||||
<string name="hardware_wallet_security_feature_title">Seguridad a nivel de hardware</string>
|
||||
<string name="high_fee_warning_description">La actividad en la red es alta. Puede continuar ahora o intentarlo de nuevo más tarde, cuando las tarifas sean más bajas.</string>
|
||||
<string name="high_fee_warning_title">La tarifa de red es más alta de lo habitual.</string>
|
||||
<string name="home_button_add_existing_wallet">Agregar Billetera Existente</string>
|
||||
<string name="home_button_create_new_wallet">Crear Nueva Billetera</string>
|
||||
<string name="home_button_order">Pedir Tangem</string>
|
||||
|
|
@ -880,12 +883,14 @@
|
|||
<string name="markets_portfolio_block_add_token_title">**Añadir a su portafolio** para empezar a comprar, intercambiar o recibir este activo</string>
|
||||
<string name="markets_portfolio_block_subtitle">En su portafolio</string>
|
||||
<string name="markets_portfolio_block_title">Su portafolio</string>
|
||||
<string name="markets_portfolio_block_token_unsupported">**Token no soportado**. Este token no está soportado actualmente en la billetera.</string>
|
||||
<string name="markets_pulse_common_title">Análisis del Mercado</string>
|
||||
<string name="markets_quick_actions">Acciones rápidas</string>
|
||||
<string name="markets_search_clear_all_hints">Borrar todo</string>
|
||||
<string name="markets_search_header_title">Buscar tokens</string>
|
||||
<string name="markets_search_hint_header">Recientes</string>
|
||||
<string name="markets_search_hint_header">Búsquedas recientes</string>
|
||||
<string name="markets_search_portfolio_header">En su portafolio</string>
|
||||
<string name="markets_search_recent_tokens_header">Tokens recientes</string>
|
||||
<string name="markets_search_result_title">Resultado</string>
|
||||
<string name="markets_search_see_tokens_under_100k">Ver tokens con marketcap inferior a 100.000$</string>
|
||||
<string name="markets_search_show_tokens">Mostrar tokens</string>
|
||||
|
|
@ -1256,6 +1261,21 @@
|
|||
<string name="organize_tokens_sort_by_balance">Por saldo</string>
|
||||
<string name="organize_tokens_title">Organizar tokens</string>
|
||||
<string name="organize_tokens_ungroup">Desagrupar</string>
|
||||
<string name="promo_campaign_already_activated_subtitle">El reembolso elegible se distribuirá a:</string>
|
||||
<string name="promo_campaign_already_activated_title">Ya está inscrito en %1$s</string>
|
||||
<string name="promo_campaign_eligible_tokens">Tokens elegibles</string>
|
||||
<string name="promo_campaign_enroll">Inscribirse</string>
|
||||
<string name="promo_campaign_enroll_success_title">Se ha inscrito correctamente en %1$s</string>
|
||||
<string name="promo_campaign_not_active_subtitle">Esta campaña ya no existe o ha caducado.</string>
|
||||
<string name="promo_campaign_not_active_title">Campaña inactiva</string>
|
||||
<string name="promo_campaign_reactivation_summary_description">Gane un reembolso de 0.5% por cada intercambio superior a 500 $, en cualquier par de divisas, excepto entre stablecoins. El pago máximo es de 50 $ por intercambio.\n\nRealice cinco intercambios válidos y desbloquee una bonificación adicional de 10 $.\n\nLas recompensas se abonan semanalmente en USDT o USDC en la dirección seleccionada.</string>
|
||||
<string name="promo_campaign_select_cashback_account">Seleccione una cuenta de reembolso</string>
|
||||
<string name="promo_campaign_select_token">Seleccionar token</string>
|
||||
<string name="promo_campaign_summary_title">Inscríbase en %1$s</string>
|
||||
<string name="promo_campaign_terms_agreement">Estoy de acuerdo con %1$s</string>
|
||||
<string name="promo_campaign_terms_agreement_android">Estoy de acuerdo con</string>
|
||||
<string name="promo_campaign_terms_link">%1$s Términos</string>
|
||||
<string name="promo_campaign_whale_swap_summary_description">Consiga reembolsos en cada intercambio a partir de 10,000 $ hasta finales de Julio.\n\nLas tasas aumentan según el volumen: 0.10% a partir de 10,000 $, 0,20% a partir de 20,000 $ y 0.50% a partir de 100,000 $.\n\nPago máximo: 500 $ por intercambio y 10,000 $ por billetera por dirección de intercambio mientras dure la campaña. Quedan excluidos los intercambios de monedas estables por otras monedas estables.\n\nEl pago se realiza semanalmente en la dirección de USDT o USDC que elija.</string>
|
||||
<string name="provider_name_support">Soporte de %s</string>
|
||||
<string name="push_notification_settings_banner_description">Las notificaciones push están activadas, pero no funcionarán hasta que las autorice</string>
|
||||
<string name="push_notification_settings_banner_title">Permitir notificaciones</string>
|
||||
|
|
@ -1293,7 +1313,7 @@
|
|||
<string name="quick_action_sell_description">Venda criptomonedas de forma segura</string>
|
||||
<string name="quick_action_send_and_swap_description">Enviar con intercambio a otro token</string>
|
||||
<string name="quick_action_send_description">Enviar a otra billetera</string>
|
||||
<string name="quick_action_swap_description">Entre sus portafolios</string>
|
||||
<string name="quick_action_swap_description">Cambia una criptomoneda por otra</string>
|
||||
<string name="quick_top_up_chip_other">Otro</string>
|
||||
<string name="quick_top_up_title">Recarga rápida</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">No se requiere nota</string>
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@
|
|||
<string name="actionbutton_addfunds_title">Ajouter des fonds</string>
|
||||
<string name="actionbutton_swap_title">Échanger</string>
|
||||
<string name="actionbutton_transfer_title">Transférer</string>
|
||||
<string name="add_and_manage_sheet_manage_subtitle">Ajouter à votre portfolio</string>
|
||||
<string name="add_and_manage_sheet_manage_title">Ajouter des jetons</string>
|
||||
<string name="add_custom_token_choose_network">Choisissez le réseau</string>
|
||||
<string name="add_custom_token_title">Ajouter un jeton personnalisé</string>
|
||||
|
|
@ -268,6 +269,7 @@
|
|||
<string name="common_accounts">Comptes</string>
|
||||
<string name="common_activate">Activer</string>
|
||||
<string name="common_add">Ajouter</string>
|
||||
<string name="common_add_funds">Ajouter fonds</string>
|
||||
<string name="common_add_to_portfolio">Ajouter au portfolio</string>
|
||||
<string name="common_add_token">Ajouter un jeton</string>
|
||||
<string name="common_add_tokens">Ajouter des jetons</string>
|
||||
|
|
@ -349,6 +351,7 @@
|
|||
<string name="common_from">Du</string>
|
||||
<string name="common_from_wallet_name">De %s</string>
|
||||
<string name="common_generate_addresses">Synchroniser les adresses</string>
|
||||
<string name="common_get">Obtenir du</string>
|
||||
<string name="common_get_started">Commencer</string>
|
||||
<string name="common_get_token">Obtenir un token</string>
|
||||
<string name="common_go_to_provider">Aller au fournisseur</string>
|
||||
|
|
@ -538,6 +541,7 @@
|
|||
<string name="domain_receive_assets_onboarding_description">L’envoi d’actifs sur d’autres réseaux entraînera une perte définitive.</string>
|
||||
<string name="domain_receive_assets_onboarding_network_name">%s réseau</string>
|
||||
<string name="domain_receive_assets_onboarding_title">Envoyez des fonds en utilisant uniquement</string>
|
||||
<string name="dynamic_addresses_receive_badge">Adresse dynamique</string>
|
||||
<string name="earn_best_opportunities">Meilleures opportunités</string>
|
||||
<string name="earn_clear_filter">Effacer le filtre</string>
|
||||
<string name="earn_filter_all_networks">Tous les réseaux</string>
|
||||
|
|
@ -658,6 +662,7 @@
|
|||
<string name="generic_error">Une erreur s\'est produite</string>
|
||||
<string name="generic_error_code">Une erreur s\'est produite. Code:%s</string>
|
||||
<string name="generic_requires_memo_error">Un mémo est requis</string>
|
||||
<string name="get_token_title">Obtenir des %1$s</string>
|
||||
<string name="give_permission_current_transaction">Cette transaction</string>
|
||||
<string name="give_permission_policy_type_footer">Spécifiez la limite approuvée pour le jeton sélectionné</string>
|
||||
<string name="give_permission_rows_amount">Montant %s</string>
|
||||
|
|
@ -673,6 +678,8 @@
|
|||
<string name="hardware_wallet_key_feature_title">Génération de clés</string>
|
||||
<string name="hardware_wallet_security_feature_description">Toutes les opérations cryptographiques s\'effectuent à l\'intérieur de la puce sécurisée, certifiée contre le clonage et la falsification physique.</string>
|
||||
<string name="hardware_wallet_security_feature_title">Sécurité au niveau matériel</string>
|
||||
<string name="high_fee_warning_description">L\'activité du réseau est élevée. Vous pouvez continuer maintenant ou réessayer plus tard, lorsque les frais seront peut-être moins élevés.</string>
|
||||
<string name="high_fee_warning_title">Les frais de réseau sont plus élevés que d\'habitude</string>
|
||||
<string name="home_button_add_existing_wallet">Ajouter un Portefeuille existant</string>
|
||||
<string name="home_button_create_new_wallet">Créer un nouveau Portefeuille</string>
|
||||
<string name="home_button_order">Commandez</string>
|
||||
|
|
@ -821,12 +828,19 @@
|
|||
<string name="markets_insights_info_description_message">Les données de cette section proviennent des réseaux suivants : %s</string>
|
||||
<string name="markets_loading_error_title">Impossible de charger les données…</string>
|
||||
<string name="markets_loading_no_data_title">Aucune donnée</string>
|
||||
<string name="markets_portfolio_block_add_token_title">**Ajouter à votre portfolio** pour acheter, échanger ou recevoir cet asset</string>
|
||||
<string name="markets_portfolio_block_subtitle">Dans votre portfolio</string>
|
||||
<string name="markets_portfolio_block_title">Votre portfolio</string>
|
||||
<string name="markets_portfolio_block_token_unsupported">**Token non pris en charge**. Ce token n\'est actuellement pas pris en charge dans le portefeuille</string>
|
||||
<string name="markets_pulse_common_title">Rythme du marché</string>
|
||||
<string name="markets_quick_actions">Actions rapides</string>
|
||||
<string name="markets_search_header_title">Rechercher sur le marché</string>
|
||||
<string name="markets_search_hint_header">Recherches récentes</string>
|
||||
<string name="markets_search_recent_tokens_header">Tokens récents</string>
|
||||
<string name="markets_search_result_title">Résultat</string>
|
||||
<string name="markets_search_see_tokens_under_100k">Voir les jetons de moins de 100 000 $ de capitalisation boursière</string>
|
||||
<string name="markets_search_show_tokens">Afficher les jetons</string>
|
||||
<string name="markets_search_title_placeholder">Crypto, actualités et plus</string>
|
||||
<string name="markets_search_token_no_result_title">Aucun résultat</string>
|
||||
<string name="markets_select_network">Sélectionnez un réseau</string>
|
||||
<string name="markets_select_wallet">Sélectionnez un portefeuille</string>
|
||||
|
|
@ -895,6 +909,7 @@
|
|||
<string name="markets_token_details_market_rating_description">Position dans le classement des crypto-monnaies entre toutes les pièces en fonction de la capitalisation boursière</string>
|
||||
<string name="markets_token_details_market_rating_full">Évaluation du marché</string>
|
||||
<string name="markets_token_details_max_supply">Approvisionnement maximal</string>
|
||||
<string name="markets_token_details_max_supply_and_circulation_full">Offre en circulation et offre maximale</string>
|
||||
<string name="markets_token_details_max_supply_description">Le nombre maximal de pièces ou de jetons pouvant exister pour une crypto-monnaie particulière</string>
|
||||
<string name="markets_token_details_max_supply_full">Approvisionnement maximal</string>
|
||||
<string name="markets_token_details_metrics">Métriques</string>
|
||||
|
|
@ -910,6 +925,7 @@
|
|||
<string name="markets_token_details_trading_volume">Volume des échanges (24h)</string>
|
||||
<string name="markets_token_details_trading_volume_24h_description">Le montant total d\'une crypto-monnaie qui a été échangé au cours des dernières 24 heures, indiquant le niveau d\'activité et de liquidité du marché</string>
|
||||
<string name="markets_token_details_trading_volume_full">Volume des échanges (24h)</string>
|
||||
<string name="markets_token_details_valuation_value_in_total">%s au total</string>
|
||||
<string name="markets_token_details_volume">Volume</string>
|
||||
<string name="markets_tooltip_message">Tirez vers le haut ou appuyez sur la barre de recherche pour ajouter des jetons directement depuis le marché</string>
|
||||
<string name="markets_tooltip_title">Ajouter des jetons</string>
|
||||
|
|
@ -1163,6 +1179,8 @@
|
|||
<string name="onramp_title_available_from">Disponible auprès de</string>
|
||||
<string name="onramp_title_available_up_to">Disponible jusqu\'à</string>
|
||||
<string name="onramp_title_you_get">Vous obtenez</string>
|
||||
<string name="onramp_token_is_not_supported_banner_subtitle">Ce token n\'est pas pris en charge. Veuillez choisir un autre token à acheter.</string>
|
||||
<string name="onramp_token_is_not_supported_banner_title">%s n\'est pas pris en charge</string>
|
||||
<string name="onramp_tos_external_providers">Le service est fourni par un prestataire externe. Tangem n\'est pas responsable.</string>
|
||||
<string name="onramp_transaction_status_footer_text">Vous pouvez vérifier l\'état de la transaction à partir de la page du jeton</string>
|
||||
<string name="onramp_up_to_rate">Jusqu\'à</string>
|
||||
|
|
@ -1175,6 +1193,21 @@
|
|||
<string name="organize_tokens_sort_by_balance">Par solde</string>
|
||||
<string name="organize_tokens_title">Organiser les jetons</string>
|
||||
<string name="organize_tokens_ungroup">Dégrouper</string>
|
||||
<string name="promo_campaign_already_activated_subtitle">Le cashback éligible sera distribué à :</string>
|
||||
<string name="promo_campaign_already_activated_title">Vous êtes déjà inscrit(e) à %1$s</string>
|
||||
<string name="promo_campaign_eligible_tokens">Tokens éligibles</string>
|
||||
<string name="promo_campaign_enroll">S\'inscrire</string>
|
||||
<string name="promo_campaign_enroll_success_title">Vous êtes bien inscrit(e) à %1$s</string>
|
||||
<string name="promo_campaign_not_active_subtitle">Cette campagne n\'existe plus ou a expiré</string>
|
||||
<string name="promo_campaign_not_active_title">Campagne non active</string>
|
||||
<string name="promo_campaign_reactivation_summary_description">Gagnez 0,5% de cashback sur chaque swap de plus de 500 $, sur toute paire sauf stablecoin vers stablecoin. Gain maximum : 50 $ par swap.\n\nComplétez cinq swaps éligibles et débloquez un bonus supplémentaire de 10 $.\n\nLes récompenses sont versées chaque semaine en USDT ou USDC à l\'adresse sélectionnée.</string>
|
||||
<string name="promo_campaign_select_cashback_account">Sélectionner le compte pour recevoir le cashback</string>
|
||||
<string name="promo_campaign_select_token">Sélectionner le token</string>
|
||||
<string name="promo_campaign_summary_title">S\'inscrire à %1$s</string>
|
||||
<string name="promo_campaign_terms_agreement">J\'accepte les %1$s</string>
|
||||
<string name="promo_campaign_terms_agreement_android">J\'accepte les</string>
|
||||
<string name="promo_campaign_terms_link">Conditions de %1$s</string>
|
||||
<string name="promo_campaign_whale_swap_summary_description">Gagnez du cashback sur chaque swap à partir de 10 000 $ jusqu\'à fin juillet.\n\nLes taux augmentent selon le montant : 0,10% à partir de 10 000 $, 0,20% à partir de 20 000 $, 0,50% à partir de 100 000 $.\n\nGain maximum : 500 $ par swap, et 10 000 $ par wallet et par sens de swap pendant toute la durée de la campagne. Les swaps stablecoin vers stablecoin sont exclus.\n\nLe paiement arrive chaque semaine à l\'adresse USDT ou USDC de votre choix.</string>
|
||||
<string name="provider_name_support">Assistance %s</string>
|
||||
<string name="push_notification_settings_banner_description">Les notifications push sont activées mais ne fonctionneront pas avant que vous les autorisiez</string>
|
||||
<string name="push_notification_settings_banner_title">Autoriser les notifications</string>
|
||||
|
|
@ -1200,7 +1233,12 @@
|
|||
<string name="qr_scanner_error_unsupported_network_title">Aucun jeton pris en charge n\'a été trouvé</string>
|
||||
<string name="qr_scanner_warning_unknown_parameters_message">Ce code QR contient des paramètres non reconnus : %s. Si vous continuez, certaines informations de paiement risquent d\'être perdues.</string>
|
||||
<string name="qr_scanner_warning_unknown_parameters_title">Paramètres inconnus</string>
|
||||
<string name="quick_action_buy_description">Carte ou compte en banque</string>
|
||||
<string name="quick_action_receive_description">Partager votre adresse ou code QR</string>
|
||||
<string name="quick_action_sell_description">Vendre de la crypto en sécurité</string>
|
||||
<string name="quick_action_send_and_swap_description">Envoyer en échangeant vers un autre token</string>
|
||||
<string name="quick_action_send_description">Envoyer vers un autre portefeuille</string>
|
||||
<string name="quick_action_swap_description">Une crypto contre une autre</string>
|
||||
<string name="quick_top_up_title">Recharge rapide</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">Aucun mémo requis</string>
|
||||
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) sur le réseau %3$s</string>
|
||||
|
|
@ -1614,6 +1652,7 @@
|
|||
<string name="tangem_pay_beta_notification_title">Tangem Pay en version bêta</string>
|
||||
<string name="tangem_pay_card_frozen">Carte gelée</string>
|
||||
<string name="tangem_pay_card_payment">Paiement par carte</string>
|
||||
<string name="tangem_pay_close_card_disabled_last_card">Vous ne pouvez pas clôturer la dernière carte</string>
|
||||
<string name="tangem_pay_close_card_popup_description">Il disparaîtra de l’application</string>
|
||||
<string name="tangem_pay_close_card_popup_primary_button_title">Clôturer la carte</string>
|
||||
<string name="tangem_pay_close_card_popup_secondary_button_title">Retour</string>
|
||||
|
|
@ -1776,6 +1815,7 @@
|
|||
<string name="tangempay_kyc_rejected_description">Désolé, nous n\'avons pas pu vérifier</string>
|
||||
<string name="tangempay_kyc_rejected_description_span">votre identité.</string>
|
||||
<string name="tangempay_maximum_cards_issued_description">Vous pouvez avoir jusqu\'à 3 cartes. Supprimez une carte pour pouvoir en ajouter une nouvelle.</string>
|
||||
<string name="tangempay_maximum_cards_issued_title">Nombre maximum de cartes émises</string>
|
||||
<string name="tangempay_newonboard_Q1_body">Oui — pour utiliser une carte Visa réglementée, la vérification d’identité est obligatoire. Le KYC est géré par Sumsub, partenaire conformité.</string>
|
||||
<string name="tangempay_newonboard_Q1_title">Dois-je fournir mes documents ?</string>
|
||||
<string name="tangempay_newonboard_Q2_body">Non. Le KYC s’applique uniquement au compte Tangem Pay. Votre Tangem Wallet reste un environnement distinct, en self-custody et sans KYC.</string>
|
||||
|
|
@ -1820,6 +1860,9 @@
|
|||
<string name="tangempay_reissue_card_insufficient_funds_subtitle">Déposez des USDC sur le compte de paiement pour couvrir les frais d’émission</string>
|
||||
<string name="tangempay_reissue_card_insufficient_funds_title">Impossible de couvrir les frais</string>
|
||||
<string name="tangempay_reissue_card_title">Réémettre votre carte ?</string>
|
||||
<string name="tangempay_remove_account">Retirer le compte</string>
|
||||
<string name="tangempay_remove_account_alert_description">Tangem Pay sera retiré de l\'écran principal et ne réapparaîtra plus, même après une réinstallation de l\'application.</string>
|
||||
<string name="tangempay_remove_account_alert_title">Retirer le compte?</string>
|
||||
<string name="tangempay_service_unavailable_description">Nous réparons un problème technique. Veuillez réessayer plus tard.</string>
|
||||
<string name="tangempay_service_unavailable_title">Service temporairement indisponible</string>
|
||||
<string name="tangempay_service_unreachable_try_later">Les données ne peuvent pas être affichées pour le moment, mais les paiements par carte fonctionnent toujours.</string>
|
||||
|
|
@ -2358,6 +2401,7 @@
|
|||
<string name="yield_module_approve_sheet_fee_note">Les frais seront prélevés et vos actifs seront à nouveau prêtés.</string>
|
||||
<string name="yield_module_approve_sheet_subtitle">Pour continuer à percevoir des revenus, une autorisation est nécessaire.</string>
|
||||
<string name="yield_module_approve_sheet_title">Confirmer l\'approbation</string>
|
||||
<string name="yield_module_average_apy">APY actuel %1$s%%</string>
|
||||
<string name="yield_module_balance_info_sheet_subtitle">Vos fonds sont actuellement affectés au protocole Aave, mais vous pouvez les gérer à tout moment.</string>
|
||||
<string name="yield_module_balance_info_sheet_title">Vos %s sont déposés dans Aave.</string>
|
||||
<string name="yield_module_chart_loading_error">Impossible de charger le graphique...</string>
|
||||
|
|
|
|||
|
|
@ -717,6 +717,7 @@
|
|||
<string name="generic_error">エラーが発生しました</string>
|
||||
<string name="generic_error_code">エラーが発生しました。コード: %s 。</string>
|
||||
<string name="generic_requires_memo_error">メモが必要</string>
|
||||
<string name="get_token_title">%1$sを入手</string>
|
||||
<string name="give_permission_current_transaction">取引</string>
|
||||
<string name="give_permission_policy_type_footer">承認すると、このスマートコントラクトが今後の取引でトークンを利用できるようになります。</string>
|
||||
<string name="give_permission_rows_amount">金額%s</string>
|
||||
|
|
@ -733,6 +734,8 @@
|
|||
<string name="hardware_wallet_key_feature_title">鍵生成</string>
|
||||
<string name="hardware_wallet_security_feature_description">すべての暗号処理は、複製や物理的な改ざんに対する認証を受けたセキュアチップ内で行われます。</string>
|
||||
<string name="hardware_wallet_security_feature_title">ハードウェアレベルのセキュリティ</string>
|
||||
<string name="high_fee_warning_description">ネットワークが混雑しています。今すぐ続けることもできますが、後でお試しいただくと、手数料が安くなる場合があります。</string>
|
||||
<string name="high_fee_warning_title">ネットワーク手数料が通常より高くなっています</string>
|
||||
<string name="home_button_add_existing_wallet">既存のウォレットを追加</string>
|
||||
<string name="home_button_create_new_wallet">新しいウォレットを作成する</string>
|
||||
<string name="home_button_order">Tangemを注文</string>
|
||||
|
|
@ -887,12 +890,14 @@
|
|||
<string name="markets_portfolio_block_add_token_title">**ポートフォリオに追加して**、この資産の買付・交換・受け取りを始めましょう</string>
|
||||
<string name="markets_portfolio_block_subtitle">ポートフォリオ内</string>
|
||||
<string name="markets_portfolio_block_title">ポートフォリオ</string>
|
||||
<string name="markets_portfolio_block_token_unsupported">**トークンはサポートされていません**。このトークンは現在、このウォレットではサポートされていません</string>
|
||||
<string name="markets_pulse_common_title">マーケット動向</string>
|
||||
<string name="markets_quick_actions">クイックアクション</string>
|
||||
<string name="markets_search_clear_all_hints">すべてクリア</string>
|
||||
<string name="markets_search_header_title">トークンを探す</string>
|
||||
<string name="markets_search_hint_header">最近の検索</string>
|
||||
<string name="markets_search_portfolio_header">ポートフォリオ内</string>
|
||||
<string name="markets_search_recent_tokens_header">最近表示したトークン</string>
|
||||
<string name="markets_search_result_title">結果</string>
|
||||
<string name="markets_search_see_tokens_under_100k">時価総額10万ドル以下のトークンを見る</string>
|
||||
<string name="markets_search_show_tokens">トークンを表示</string>
|
||||
|
|
@ -1238,6 +1243,8 @@
|
|||
<string name="onramp_title_available_from">利用可能:</string>
|
||||
<string name="onramp_title_available_up_to">利用可能金額</string>
|
||||
<string name="onramp_title_you_get">以下が手に入ります。</string>
|
||||
<string name="onramp_token_is_not_supported_banner_subtitle">このトークンはサポートされていません。別のトークンを選択して購入してください。</string>
|
||||
<string name="onramp_token_is_not_supported_banner_title">%sはサポートされていません</string>
|
||||
<string name="onramp_tos_external_providers">サービスは外部プロバイダーによって提供されます。 \nTangemは責任を負いません。</string>
|
||||
<string name="onramp_transaction_status_footer_text">この画面を閉じて、トークンの詳細画面で取引状況を確認できます。</string>
|
||||
<string name="onramp_up_to_rate">最大</string>
|
||||
|
|
@ -1301,7 +1308,7 @@
|
|||
<string name="quick_action_sell_description">暗号資産を安全に売却</string>
|
||||
<string name="quick_action_send_and_swap_description">別のトークンにスワップして送る</string>
|
||||
<string name="quick_action_send_description">別のウォレットに送信</string>
|
||||
<string name="quick_action_swap_description">ポートフォリオ間で</string>
|
||||
<string name="quick_action_swap_description">暗号資産を別の暗号資産に交換</string>
|
||||
<string name="quick_top_up_chip_other">その他</string>
|
||||
<string name="quick_top_up_title">クイック入金</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">メモ不要</string>
|
||||
|
|
@ -2511,7 +2518,7 @@
|
|||
<string name="yield_module_approve_sheet_fee_note">手数料が差し引かれ、暗号資産が補充されます。</string>
|
||||
<string name="yield_module_approve_sheet_subtitle">利息を継続的に生み出すには承認が必要です。</string>
|
||||
<string name="yield_module_approve_sheet_title">承認を確定する</string>
|
||||
<string name="yield_module_average_apy">平均APY %1$s%%</string>
|
||||
<string name="yield_module_average_apy">現在のAPY %1$s%%</string>
|
||||
<string name="yield_module_balance_info_sheet_subtitle">あなたの資金は現在Aaveプロトコルに預けられていますが、いつでも自由に管理できます。</string>
|
||||
<string name="yield_module_balance_info_sheet_title">%sはAaveに供給されています</string>
|
||||
<string name="yield_module_chart_loading_error">チャートを読み込めません・・</string>
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@
|
|||
<string name="address_book_no_contacts">Ainda não há contatos.</string>
|
||||
<string name="address_book_no_contacts_description">Os contatos que você adicionar aparecerão aqui.</string>
|
||||
<string name="address_book_remove_address">Remover endereço</string>
|
||||
<string name="address_book_save_address">Salvar endereço</string>
|
||||
<string name="address_book_save_contact">Salvar contato</string>
|
||||
<string name="address_book_save_to_wallet_title">Salvar na carteira</string>
|
||||
<string name="address_book_save_wallet_to_description">Este contato será vinculado à agenda de endereços desta carteira.</string>
|
||||
|
|
@ -638,6 +639,8 @@
|
|||
<string name="express_exchange_notification_long_transaction_time_title">Tempo de transação longo</string>
|
||||
<string name="express_exchange_notification_refund_text">O valor da transação foi reembolsado em %1$s para sua carteira devido às regras do OKX ou da ponte. %2$s</string>
|
||||
<string name="express_exchange_notification_refund_title">O valor foi reembolsado em %1$s (%2$s rede)</string>
|
||||
<string name="express_exchange_notification_refunded_in_text">Seus fundos foram reembolsados em %1$s para sua carteira no %2$s rede, de acordo com as regras da bolsa OKX.</string>
|
||||
<string name="express_exchange_notification_refunded_in_title">Reembolsado em %s</string>
|
||||
<string name="express_exchange_notification_verification_text">Visite o site do fornecedor para verificação.</string>
|
||||
<string name="express_exchange_notification_verification_title">Verificação KYC exigida pelo fornecedor</string>
|
||||
<string name="express_exchange_status_bought">Compra concluída</string>
|
||||
|
|
@ -924,12 +927,14 @@
|
|||
<string name="markets_portfolio_block_subtitle">Em seu portfólio</string>
|
||||
<string name="markets_portfolio_block_title">Seu portfólio</string>
|
||||
<string name="markets_portfolio_block_token_unsupported">**Token não suportado**. Este token não é suportado na carteira neste momento.</string>
|
||||
<string name="markets_portfolio_eligible_block_title">Outros tokens elegíveis</string>
|
||||
<string name="markets_pulse_common_title">Pulso do mercado</string>
|
||||
<string name="markets_quick_actions">Ações rápidas</string>
|
||||
<string name="markets_search_clear_all_hints">Limpar tudo</string>
|
||||
<string name="markets_search_header_title">Pesquisar tokens</string>
|
||||
<string name="markets_search_hint_header">Recentes</string>
|
||||
<string name="markets_search_hint_header">Pesquisas recentes</string>
|
||||
<string name="markets_search_portfolio_header">Em seu portfólio</string>
|
||||
<string name="markets_search_recent_tokens_header">Tokens recentes</string>
|
||||
<string name="markets_search_result_title">Resultado</string>
|
||||
<string name="markets_search_see_tokens_under_100k">Veja tokens com capitalização de mercado inferior a 100 mil dólares.</string>
|
||||
<string name="markets_search_show_tokens">Mostrar tokens</string>
|
||||
|
|
@ -1305,9 +1310,11 @@
|
|||
<string name="promo_campaign_eligible_tokens">Tokens elegíveis</string>
|
||||
<string name="promo_campaign_enroll">Inscreva-se</string>
|
||||
<string name="promo_campaign_enroll_success_title">Você se inscreveu com sucesso em %1$s</string>
|
||||
<string name="promo_campaign_not_active_subtitle">Esta campanha não existe mais ou expirou.</string>
|
||||
<string name="promo_campaign_not_active_subtitle">Esta campanha não existe ou expirou.</string>
|
||||
<string name="promo_campaign_not_active_title">Campanha inativa</string>
|
||||
<string name="promo_campaign_reactivation_summary_description">Ganhe 0,5% Cashback em todas as trocas acima de US$ 500, em qualquer par, exceto entre pares estáveis. Pagamento máximo de US$ 50 por troca.\n\nComplete cinco trocas qualificadas e desbloqueie um bônus extra de US$ 10.\n\nAs recompensas são pagas semanalmente em USDT ou USDC no endereço selecionado.</string>
|
||||
<string name="promo_campaign_select_cashback_account">Selecione a conta de cashback</string>
|
||||
<string name="promo_campaign_select_token">Selecionar token</string>
|
||||
<string name="promo_campaign_summary_title">Inscreva-se em %1$s</string>
|
||||
<string name="promo_campaign_terms_agreement">Concordo com %1$s</string>
|
||||
<string name="promo_campaign_terms_agreement_android">Concordo com</string>
|
||||
|
|
@ -1350,7 +1357,7 @@
|
|||
<string name="quick_action_sell_description">Venda criptomoedas com segurança</string>
|
||||
<string name="quick_action_send_and_swap_description">Enviar com troca para outro token</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_action_swap_description">Uma cripto por outra</string>
|
||||
<string name="quick_top_up_chip_other">Outro</string>
|
||||
<string name="quick_top_up_title">Recarga rápida</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">Não é necessário memorando</string>
|
||||
|
|
@ -1975,6 +1982,7 @@
|
|||
<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_main_select_plan">Selecione o plano</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_newonboard_Q1_body">Sim — para usar um cartão Visa regulado, a verificação de identidade é obrigatória. O KYC é feito pela Sumsub, parceira de compliance.</string>
|
||||
|
|
|
|||
|
|
@ -377,7 +377,7 @@
|
|||
<string name="common_from">Из</string>
|
||||
<string name="common_from_wallet_name">Из %s</string>
|
||||
<string name="common_generate_addresses">Синхронизировать адреса</string>
|
||||
<string name="common_get">Приобрести</string>
|
||||
<string name="common_get">Пополнить</string>
|
||||
<string name="common_get_started">Начать</string>
|
||||
<string name="common_get_token">Получить токен</string>
|
||||
<string name="common_go_to_provider">К провайдеру</string>
|
||||
|
|
@ -749,6 +749,7 @@
|
|||
<string name="generic_error">Произошла ошибка</string>
|
||||
<string name="generic_error_code">Произошла ошибка. Код: %s.</string>
|
||||
<string name="generic_requires_memo_error">Требуется memo</string>
|
||||
<string name="get_token_title">Пополнить %1$s</string>
|
||||
<string name="give_permission_current_transaction">Транзакция</string>
|
||||
<string name="give_permission_policy_type_footer">Укажите лимит доступа к выбранному токену</string>
|
||||
<string name="give_permission_rows_amount">Количество %s</string>
|
||||
|
|
@ -765,6 +766,8 @@
|
|||
<string name="hardware_wallet_key_feature_title">Генерация ключа</string>
|
||||
<string name="hardware_wallet_security_feature_description">Операции с криптографией выполняются внутри защищённого чипа, устойчивого к клонированию и физическому взлому.</string>
|
||||
<string name="hardware_wallet_security_feature_title">Аппаратная безопасность</string>
|
||||
<string name="high_fee_warning_description">Высокая активность в сети. Вы можете продолжить сейчас или попробовать позже, когда комиссии будут ниже.</string>
|
||||
<string name="high_fee_warning_title">Комиссия сети выше, чем обычно</string>
|
||||
<string name="home_button_add_existing_wallet">Добавить существующий кошелек</string>
|
||||
<string name="home_button_create_new_wallet">Создать новый кошелек</string>
|
||||
<string name="home_button_order">Купить</string>
|
||||
|
|
@ -922,12 +925,14 @@
|
|||
<string name="markets_portfolio_block_add_token_title">**Добавьте в свой портфель**, чтобы покупать, обменивать или получать этот актив.</string>
|
||||
<string name="markets_portfolio_block_subtitle">В вашем портфеле</string>
|
||||
<string name="markets_portfolio_block_title">Ваш портфель</string>
|
||||
<string name="markets_portfolio_block_token_unsupported">**Токен не поддерживается**. В настоящее время этот токен не поддерживается в кошельке</string>
|
||||
<string name="markets_pulse_common_title">Пульс рынка</string>
|
||||
<string name="markets_quick_actions">Быстрые действия</string>
|
||||
<string name="markets_search_clear_all_hints">Очистить всё</string>
|
||||
<string name="markets_search_header_title">Поиск токенов</string>
|
||||
<string name="markets_search_hint_header">Недавние</string>
|
||||
<string name="markets_search_hint_header">Недавние запросы</string>
|
||||
<string name="markets_search_portfolio_header">В вашем портфеле</string>
|
||||
<string name="markets_search_recent_tokens_header">Недавние токены</string>
|
||||
<string name="markets_search_result_title">Результат</string>
|
||||
<string name="markets_search_see_tokens_under_100k">Токены с капитализацией меньше 100к USD</string>
|
||||
<string name="markets_search_show_tokens">Показать токены</string>
|
||||
|
|
@ -1306,6 +1311,8 @@
|
|||
<string name="onramp_title_available_from">Доступно от</string>
|
||||
<string name="onramp_title_available_up_to">Доступно до</string>
|
||||
<string name="onramp_title_you_get">Вы получите</string>
|
||||
<string name="onramp_token_is_not_supported_banner_subtitle">Этот токен не поддерживается. Пожалуйста, выберите другой токен для покупки.</string>
|
||||
<string name="onramp_token_is_not_supported_banner_title">%s не поддерживается</string>
|
||||
<string name="onramp_tos_external_providers">Услуга предоставляется сторонним провайдером.\nTangem не несет ответственности.</string>
|
||||
<string name="onramp_transaction_status_footer_text">Вы можете закрыть этот экран и проверить статус транзакции на экране информации о токене.</string>
|
||||
<string name="onramp_up_to_rate">До</string>
|
||||
|
|
@ -1318,6 +1325,20 @@
|
|||
<string name="organize_tokens_sort_by_balance">По балансу</string>
|
||||
<string name="organize_tokens_title">Упорядочить токены</string>
|
||||
<string name="organize_tokens_ungroup">Список</string>
|
||||
<string name="promo_campaign_already_activated_subtitle">Начисленный кешбэк будет отправлен на:</string>
|
||||
<string name="promo_campaign_already_activated_title">Вы уже участвуете в %1$s</string>
|
||||
<string name="promo_campaign_eligible_tokens">Доступные токены</string>
|
||||
<string name="promo_campaign_enroll">Участвовать</string>
|
||||
<string name="promo_campaign_enroll_success_title">Вы успешно стали участником в %1$s</string>
|
||||
<string name="promo_campaign_not_active_subtitle">Эта кампания больше не существует или срок ее действия истек</string>
|
||||
<string name="promo_campaign_not_active_title">Кампания неактивна</string>
|
||||
<string name="promo_campaign_reactivation_summary_description">Получайте 0.5% кешбэка за каждый обмен от $500 в любой паре, кроме стейблкоина на стейблкоин. Максимальная выплата: $50 за обмен.\n\nСовершите пять соответствующих условиям обменов и разблокируйте дополнительный бонус в $10.\n\nНаграды выплачиваются еженедельно в USDT или USDC на выбранный адрес.</string>
|
||||
<string name="promo_campaign_select_cashback_account">Выберите счет для кешбэка</string>
|
||||
<string name="promo_campaign_select_token">Выберите токен</string>
|
||||
<string name="promo_campaign_summary_title">Участвовать в %1$s</string>
|
||||
<string name="promo_campaign_terms_agreement">Я принимаю %1$s</string>
|
||||
<string name="promo_campaign_terms_link">%1$s Условия</string>
|
||||
<string name="promo_campaign_whale_swap_summary_description">Получайте кешбэк за каждый обмен от $10K до конца июля.\n\nСтавка растет вместе с суммой: 0.10% от $10K, 0.20% от $20K, 0.50% от $100K.\n\nМаксимальная выплата: $500 за обмен и $10,000 на кошелек для каждого направления обмена, пока длится кампания. Обмены стейблкоина на стейблкоин исключены.\n\nВыплата поступает еженедельно на выбранный вами адрес USDT или USDC.</string>
|
||||
<string name="provider_name_support">Поддержка %s</string>
|
||||
<string name="push_notification_settings_banner_description">Push-уведомления включены, но не будут работать без вашего разрешения</string>
|
||||
<string name="push_notification_settings_banner_title">Разрешить уведомления</string>
|
||||
|
|
@ -1350,12 +1371,12 @@
|
|||
<string name="qr_scanner_error_unsupported_network_title">Поддерживаемые токены не найдены</string>
|
||||
<string name="qr_scanner_warning_unknown_parameters_message">Этот QR-код содержит параметры, которые не распознаны: %s. Некоторые данные платежа могут быть утеряны, если вы продолжите.</string>
|
||||
<string name="qr_scanner_warning_unknown_parameters_title">Неизвестные параметры</string>
|
||||
<string name="quick_action_buy_description">Банковская карта или банковский счет</string>
|
||||
<string name="quick_action_buy_description">Криптовалюту картой, переводом и не только</string>
|
||||
<string name="quick_action_receive_description">Поделитесь своим адресом или QR-кодом</string>
|
||||
<string name="quick_action_sell_description">Продавайте криптовалюту безопасно</string>
|
||||
<string name="quick_action_send_and_swap_description">Отправить с обменом на другой токен</string>
|
||||
<string name="quick_action_send_description">Отправить на другой кошелек</string>
|
||||
<string name="quick_action_swap_description">Между вашими портфелями</string>
|
||||
<string name="quick_action_swap_description">Обменяйте одну криптовалюту на другую</string>
|
||||
<string name="quick_top_up_chip_other">Другие</string>
|
||||
<string name="quick_top_up_title">Быстрое пополнение</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">Memo не требуется</string>
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@
|
|||
<string name="common_from">З</string>
|
||||
<string name="common_from_wallet_name">З %s</string>
|
||||
<string name="common_generate_addresses">Синхронізувати адреси</string>
|
||||
<string name="common_get">Поповнити</string>
|
||||
<string name="common_get_started">Почати</string>
|
||||
<string name="common_get_token">Отримати токен</string>
|
||||
<string name="common_go_to_provider">Перейти до провайдера</string>
|
||||
|
|
@ -748,6 +749,7 @@
|
|||
<string name="generic_error">Виникла помилка</string>
|
||||
<string name="generic_error_code">Виникла помилка. Код: %s.</string>
|
||||
<string name="generic_requires_memo_error">Вимагається memo</string>
|
||||
<string name="get_token_title">Поповнити %1$s</string>
|
||||
<string name="give_permission_current_transaction">Транзакція</string>
|
||||
<string name="give_permission_policy_type_footer">Вкажіть ліміт доступу для обраного токена</string>
|
||||
<string name="give_permission_rows_amount">Кількість %s</string>
|
||||
|
|
@ -764,6 +766,8 @@
|
|||
<string name="hardware_wallet_key_feature_title">Генерація ключа</string>
|
||||
<string name="hardware_wallet_security_feature_description">Операції з криптографією відбуваються всередині захищеного чіпу, стійкого до клонування та фізичному взлому.</string>
|
||||
<string name="hardware_wallet_security_feature_title">Безпека на апаратному рівні</string>
|
||||
<string name="high_fee_warning_description">Висока активність у мережі. Ви можете продовжити зараз або спробувати пізніше, коли комісії будуть нижчими.</string>
|
||||
<string name="high_fee_warning_title">Комісія мережі вища, ніж зазвичай</string>
|
||||
<string name="home_button_add_existing_wallet">Додати існуючий гаманець</string>
|
||||
<string name="home_button_create_new_wallet">Створити новий гаманець</string>
|
||||
<string name="home_button_order">Купити</string>
|
||||
|
|
@ -921,12 +925,14 @@
|
|||
<string name="markets_portfolio_block_add_token_title">**Додайте до свого портфеля**, щоб купувати, обмінювати або отримувати цей актив</string>
|
||||
<string name="markets_portfolio_block_subtitle">У вашому портфелі</string>
|
||||
<string name="markets_portfolio_block_title">Ваш портфель</string>
|
||||
<string name="markets_portfolio_block_token_unsupported">**Токен не підтримується**. Наразі цей токен не підтримується в гаманці</string>
|
||||
<string name="markets_pulse_common_title">Пульс ринку</string>
|
||||
<string name="markets_quick_actions">Швидкі дії</string>
|
||||
<string name="markets_search_clear_all_hints">Очистити все</string>
|
||||
<string name="markets_search_header_title">Шукати на маркеті</string>
|
||||
<string name="markets_search_hint_header">Нещодавні</string>
|
||||
<string name="markets_search_hint_header">Нещодавні запити</string>
|
||||
<string name="markets_search_portfolio_header">У вашому портфелі</string>
|
||||
<string name="markets_search_recent_tokens_header">Нещодавні токени</string>
|
||||
<string name="markets_search_result_title">Результат</string>
|
||||
<string name="markets_search_see_tokens_under_100k">Переглянути токени до 100к USD ринкової капіталізації</string>
|
||||
<string name="markets_search_show_tokens">Показати токени</string>
|
||||
|
|
@ -1305,6 +1311,8 @@
|
|||
<string name="onramp_title_available_from">Доступно з</string>
|
||||
<string name="onramp_title_available_up_to">Доступно до</string>
|
||||
<string name="onramp_title_you_get">Ви отримаєте</string>
|
||||
<string name="onramp_token_is_not_supported_banner_subtitle">Цей токен не підтримується. Будь ласка, виберіть інший токен для купівлі.</string>
|
||||
<string name="onramp_token_is_not_supported_banner_title">%s не підтримується</string>
|
||||
<string name="onramp_tos_external_providers">Послуга надається зовнішнім провайдером. \nTangem не несе відповідальності.</string>
|
||||
<string name="onramp_transaction_status_footer_text">Ви можете перевірити статус транзакції на сторінці токена</string>
|
||||
<string name="onramp_up_to_rate">До</string>
|
||||
|
|
@ -1317,6 +1325,20 @@
|
|||
<string name="organize_tokens_sort_by_balance">За балансом</string>
|
||||
<string name="organize_tokens_title">Сортування токенів</string>
|
||||
<string name="organize_tokens_ungroup">Список</string>
|
||||
<string name="promo_campaign_already_activated_subtitle">Нарахований кешбек буде надіслано на:</string>
|
||||
<string name="promo_campaign_already_activated_title">Ви вже долучилися до %1$s</string>
|
||||
<string name="promo_campaign_eligible_tokens">Доступні токени</string>
|
||||
<string name="promo_campaign_enroll">Долучитися</string>
|
||||
<string name="promo_campaign_enroll_success_title">Ви успішно долучилися до %1$s</string>
|
||||
<string name="promo_campaign_not_active_subtitle">Ця кампанія більше не існує або термін її дії закінчився</string>
|
||||
<string name="promo_campaign_not_active_title">Кампанія неактивна</string>
|
||||
<string name="promo_campaign_reactivation_summary_description">Отримуйте 0.5% кешбеку за кожний обмін від $500 у будь-якій парі, крім стейблкоїна на стейблкоїн. Максимальна виплата: $50 за обмін.\n\nЗдійсніть п\'ять відповідних умовам обмінів та розблокуйте додатковий бонус у $10.\n\nНагороди виплачуються щотижня в USDT або USDC на обрану адресу.</string>
|
||||
<string name="promo_campaign_select_cashback_account">Оберіть рахунок для кешбеку</string>
|
||||
<string name="promo_campaign_select_token">Оберіть токен</string>
|
||||
<string name="promo_campaign_summary_title">Долучитися до %1$s</string>
|
||||
<string name="promo_campaign_terms_agreement">Я приймаю %1$s</string>
|
||||
<string name="promo_campaign_terms_link">%1$s Умови</string>
|
||||
<string name="promo_campaign_whale_swap_summary_description">Отримуйте кешбек за кожний обмін від $10K до кінця липня.\n\nСтавка зростає разом із сумою: 0.10% від $10K, 0.20% від $20K, 0.50% від $100K.\n\nМаксимальна виплата: $500 за обмін та $10,000 на гаманець для кожного напрямку обмінів, поки триває кампанія. Обміни стейблкоїна на стейблкоїн виключено.\n\nВиплата надходить щотижня на обрану вами адресу USDT або USDC.</string>
|
||||
<string name="provider_name_support">Підтримка %s</string>
|
||||
<string name="push_notification_settings_banner_description">Push-сповіщення ввімкнені, але не працюватимуть без вашого дозволу</string>
|
||||
<string name="push_notification_settings_banner_title">Дозволити сповіщення</string>
|
||||
|
|
@ -1354,7 +1376,7 @@
|
|||
<string name="quick_action_sell_description">Безпечно продавайте криптовалюту</string>
|
||||
<string name="quick_action_send_and_swap_description">Надіслати з обміном на інший токен</string>
|
||||
<string name="quick_action_send_description">Надіслати на інший гаманець</string>
|
||||
<string name="quick_action_swap_description">Між вашими портфелями</string>
|
||||
<string name="quick_action_swap_description">Обміняйте одну криптовалюту на іншу</string>
|
||||
<string name="quick_top_up_chip_other">Інші</string>
|
||||
<string name="quick_top_up_title">Швидке поповнення</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">Memo не вимагається</string>
|
||||
|
|
@ -2010,6 +2032,7 @@
|
|||
<string name="tangempay_reissue_card_insufficient_funds_subtitle">Поповніть платіжний рахунок в USDC, щоб покрити комісію за випуск</string>
|
||||
<string name="tangempay_reissue_card_insufficient_funds_title">Неможливо покрити комісію</string>
|
||||
<string name="tangempay_reissue_card_title">Перевипустити картку?</string>
|
||||
<string name="tangempay_remove_account">Видалити аккаунт</string>
|
||||
<string name="tangempay_service_unavailable_description">Ми усуваємо технічну проблему. Будь ласка, спробуйте пізніше.</string>
|
||||
<string name="tangempay_service_unavailable_title">Сервіс тимчасово недоступний</string>
|
||||
<string name="tangempay_service_unreachable_try_later">Не можемо показати дані картки, але оплати продовжують працювати.</string>
|
||||
|
|
@ -2534,7 +2557,7 @@
|
|||
<string name="yield_module_approve_sheet_fee_note">Комісія буде знята, а ваші активи знову почнуть приносити дохід.</string>
|
||||
<string name="yield_module_approve_sheet_subtitle">Щоб продовжити заробляти, потрібне схвалення.</string>
|
||||
<string name="yield_module_approve_sheet_title">Підтвердити дозвіл</string>
|
||||
<string name="yield_module_average_apy">Середній APY %1$s%%</string>
|
||||
<string name="yield_module_average_apy">Поточний APY %1$s%%</string>
|
||||
<string name="yield_module_balance_info_sheet_subtitle">Наразі ваші кошти розміщені у протоколі Aave, але ви можете керувати ними в будь-який час.</string>
|
||||
<string name="yield_module_balance_info_sheet_title">Ваш %s внесений у Aave</string>
|
||||
<string name="yield_module_chart_loading_error">Неможливо завантажити графік</string>
|
||||
|
|
|
|||
|
|
@ -713,6 +713,7 @@
|
|||
<string name="generic_error">发生错误</string>
|
||||
<string name="generic_error_code">发生错误。错误代码: %s。</string>
|
||||
<string name="generic_requires_memo_error">需要备忘录</string>
|
||||
<string name="get_token_title">获取 %1$s</string>
|
||||
<string name="give_permission_current_transaction">交易</string>
|
||||
<string name="give_permission_policy_type_footer">批准后,您即允许智能合约在未来的交易中使用您的代币。</string>
|
||||
<string name="give_permission_rows_amount">金额 %s</string>
|
||||
|
|
@ -729,6 +730,8 @@
|
|||
<string name="hardware_wallet_key_feature_title">密钥生成</string>
|
||||
<string name="hardware_wallet_security_feature_description">所有加密操作都在安全芯片内部进行,该芯片经过认证,可防止克隆和物理篡改。</string>
|
||||
<string name="hardware_wallet_security_feature_title">硬件级安全</string>
|
||||
<string name="high_fee_warning_description">网络流量较大,您现在可以继续,也可以稍后再试,届时手续费可能会更低。</string>
|
||||
<string name="high_fee_warning_title">网络费用比平时高</string>
|
||||
<string name="home_button_add_existing_wallet">添加现有钱包</string>
|
||||
<string name="home_button_create_new_wallet">创建新钱包</string>
|
||||
<string name="home_button_order">订购 Tangem</string>
|
||||
|
|
@ -887,8 +890,9 @@
|
|||
<string name="markets_quick_actions">快速行动</string>
|
||||
<string name="markets_search_clear_all_hints">全部清除</string>
|
||||
<string name="markets_search_header_title">搜索代币</string>
|
||||
<string name="markets_search_hint_header">最近的</string>
|
||||
<string name="markets_search_hint_header">最近搜索</string>
|
||||
<string name="markets_search_portfolio_header">在您的投资组合中</string>
|
||||
<string name="markets_search_recent_tokens_header">近期代币</string>
|
||||
<string name="markets_search_result_title">结果</string>
|
||||
<string name="markets_search_see_tokens_under_100k">查看市值低于 10 万美元的代币</string>
|
||||
<string name="markets_search_show_tokens">显示代币</string>
|
||||
|
|
@ -1234,6 +1238,8 @@
|
|||
<string name="onramp_title_available_from">可从</string>
|
||||
<string name="onramp_title_available_up_to">最高可提供</string>
|
||||
<string name="onramp_title_you_get">你得到</string>
|
||||
<string name="onramp_token_is_not_supported_banner_subtitle">不支持该代币。请选择其他代币进行购买。</string>
|
||||
<string name="onramp_token_is_not_supported_banner_title">%s 不支持</string>
|
||||
<string name="onramp_tos_external_providers">服务由外部供应商提供。\nTangem 不承担任何责任。</string>
|
||||
<string name="onramp_transaction_status_footer_text">您可以关闭此屏幕,并在代币详情屏幕上查看交易状态。</string>
|
||||
<string name="onramp_up_to_rate">至多</string>
|
||||
|
|
@ -1940,6 +1946,9 @@
|
|||
<string name="tangempay_reissue_card_insufficient_funds_subtitle">将USDC存入支付账户以支付发行费用</string>
|
||||
<string name="tangempay_reissue_card_insufficient_funds_title">无法支付费用</string>
|
||||
<string name="tangempay_reissue_card_title">要重新发行您的卡片吗?</string>
|
||||
<string name="tangempay_remove_account">删除账户</string>
|
||||
<string name="tangempay_remove_account_alert_description">Tangem Pay 将从主屏幕中移除,即使重新安装该应用,它也不会再次出现。</string>
|
||||
<string name="tangempay_remove_account_alert_title">删除账号?</string>
|
||||
<string name="tangempay_service_unavailable_description">我们正在修复技术问题,请稍后再试。</string>
|
||||
<string name="tangempay_service_unavailable_title">服务暂时不可用</string>
|
||||
<string name="tangempay_service_unreachable_try_later">无法显示详细信息。但刷卡支付功能仍然可用。</string>
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@
|
|||
<string name="addfunds_buy_row_description">Credit card or bank account</string>
|
||||
<string name="addfunds_fund_token">Fund token</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_swap_row_description">Exchange one crypto for another</string>
|
||||
<string name="addfunds_you_receive_title">You receive</string>
|
||||
<string name="address_book_add_address">Add address</string>
|
||||
<string name="address_book_add_address_description">Add address and select network</string>
|
||||
|
|
@ -726,6 +726,11 @@
|
|||
<string name="feedback_subject_tx_failed">Can\'t send a transaction</string>
|
||||
<string name="feedback_token_description_error">Coin description error</string>
|
||||
<string name="for_you_description">Review portfolio and explore earn opportunities</string>
|
||||
<string name="for_you_earn_opportunities">Earn opportunities</string>
|
||||
<string name="for_you_earn_opportunities_all_tokens_active">All your assets are at work, explore new opportunities</string>
|
||||
<string name="for_you_earn_opportunities_no_available_tokens">Get up to %1$s annually</string>
|
||||
<string name="for_you_earn_opportunities_tokens_rewards">Max potential rewards %1$s</string>
|
||||
<string name="for_you_earn_per_year">%1$s/year</string>
|
||||
<string name="for_you_portfolio_review_title">Portfolio review</string>
|
||||
<string name="for_you_title">For You</string>
|
||||
<string name="force_update_action">Update now</string>
|
||||
|
|
@ -933,8 +938,9 @@
|
|||
<string name="markets_quick_actions">Quick actions</string>
|
||||
<string name="markets_search_clear_all_hints">Clear all</string>
|
||||
<string name="markets_search_header_title">Search tokens</string>
|
||||
<string name="markets_search_hint_header">Recent\'s</string>
|
||||
<string name="markets_search_hint_header">Recent searches</string>
|
||||
<string name="markets_search_portfolio_header">In your portfolio</string>
|
||||
<string name="markets_search_recent_tokens_header">Recent tokens</string>
|
||||
<string name="markets_search_result_title">Result</string>
|
||||
<string name="markets_search_see_tokens_under_100k">See tokens under 100k USD market cap</string>
|
||||
<string name="markets_search_show_tokens">Show tokens</string>
|
||||
|
|
@ -1352,12 +1358,12 @@
|
|||
<string name="qr_scanner_error_unsupported_network_title">No supported tokens found</string>
|
||||
<string name="qr_scanner_warning_unknown_parameters_message">This QR code contains parameters that are not recognized: %s. Some payment details may be lost if you continue.</string>
|
||||
<string name="qr_scanner_warning_unknown_parameters_title">Unknown Parameters</string>
|
||||
<string name="quick_action_buy_description">Credit card or bank account</string>
|
||||
<string name="quick_action_buy_description">Get crypto by card, bank transfer & more</string>
|
||||
<string name="quick_action_receive_description">Share your address or QR-code</string>
|
||||
<string name="quick_action_sell_description">Sell crypto securely</string>
|
||||
<string name="quick_action_send_and_swap_description">Send with swap to another token</string>
|
||||
<string name="quick_action_send_description">Send to another wallet</string>
|
||||
<string name="quick_action_swap_description">Between your portfolios</string>
|
||||
<string name="quick_action_sell_description">Convert crypto to fiat currency</string>
|
||||
<string name="quick_action_send_and_swap_description">Exchange and send in one step</string>
|
||||
<string name="quick_action_send_description">Transfer crypto to another wallet</string>
|
||||
<string name="quick_action_swap_description">Exchange one crypto for another</string>
|
||||
<string name="quick_top_up_chip_other">Other</string>
|
||||
<string name="quick_top_up_title">Quick top up</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">No memo required</string>
|
||||
|
|
@ -1986,6 +1992,7 @@
|
|||
<string name="tangempay_kyc_rejected_button_text">Hide KYC block</string>
|
||||
<string name="tangempay_kyc_rejected_description">Sorry, we couldn\'t verify</string>
|
||||
<string name="tangempay_kyc_rejected_description_span">your profile.</string>
|
||||
<string name="tangempay_main_select_plan">Select plan</string>
|
||||
<string name="tangempay_maximum_cards_issued_description">You can have up to 3 cards. Delete one to add a new card.</string>
|
||||
<string name="tangempay_maximum_cards_issued_title">Maximum Cards Issued</string>
|
||||
<string name="tangempay_newonboard_Q1_body">Yes – to operate a regulated Visa card, identity verification is mandatory. KYC is handled by Sumsub (compliance partner).</string>
|
||||
|
|
@ -2019,8 +2026,6 @@
|
|||
<string name="tangempay_onboarding_setup_wallet_title">We\'ll set up a wallet</string>
|
||||
<string name="tangempay_onboarding_title">Get your Tangem Pay Card in minutes</string>
|
||||
<string name="tangempay_pay_support">Pay Support</string>
|
||||
<string name="tangempay_main_select_plan">Select plan</string>
|
||||
<string name="tangempay_status_inactive">Inactive</string>
|
||||
<string name="tangempay_payment_account">Payment account</string>
|
||||
<string name="tangempay_payment_account_sync_needed">Session expired</string>
|
||||
<string name="tangempay_pin_validation_error_message">Invalid PIN: avoid sequences or repeats</string>
|
||||
|
|
@ -2051,6 +2056,7 @@
|
|||
<string name="tangempay_set_pin_header">Set up new PIN</string>
|
||||
<string name="tangempay_set_pin_title">Set PIN</string>
|
||||
<string name="tangempay_status_deactivated">Account closed</string>
|
||||
<string name="tangempay_status_inactive">Inactive</string>
|
||||
<string name="tangempay_status_replacing">Replacing your card</string>
|
||||
<string name="tangempay_sync_needed">Use your card or ring to renew session</string>
|
||||
<string name="tangempay_sync_needed_body">Use your card or ring to renew session</string>
|
||||
|
|
@ -2066,6 +2072,8 @@
|
|||
<string name="tangempay_topup_swap_body">Use crypto from your wallet to top up your payment account</string>
|
||||
<string name="tangempay_topup_swap_title">From your Tangem Wallet</string>
|
||||
<string name="tangempay_usdc_on_polygon_network">USDC on Polygon network</string>
|
||||
<string name="tangempay_va_banking_details_error_description">Please try again or contact support if the issue persists</string>
|
||||
<string name="tangempay_va_banking_details_error_title">Couldn\'t load banking details</string>
|
||||
<string name="tangempay_visa_benefits">Visa Benefits</string>
|
||||
<string name="tangempay_withdrawal_note_description">Funds from refunded purchases won’t be returned your on-chain Polygon balance or be available for withdrawal, but will stay on your card balance for purchases</string>
|
||||
<string name="tangempay_withdrawal_note_title">Please note</string>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
|
|||
return@flow
|
||||
}
|
||||
|
||||
val dAppUri = URI(sdkSessionProposal.url)
|
||||
val dAppUri = URI(sdkVerifyContext.getDappOriginUrl())
|
||||
if (dAppUri.host.isNullOrEmpty()) {
|
||||
emit(WcPairState.Error(WcPairError.InvalidDomainURL))
|
||||
return@flow
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ interface OfframpRepository {
|
|||
* @param cryptoCurrency crypto currency to sell
|
||||
* @param fiatCurrencyCode fiat currency code (e.g., "USD", "EUR")
|
||||
* @param walletAddress wallet address for the refund
|
||||
* @param requestId single-use nonce embedded into the provider redirect URL to authenticate the
|
||||
* returning `redirect_sell` deeplink
|
||||
* @param requestId nonce embedded into the provider redirect URL to authenticate the returning
|
||||
* `redirect_sell` deeplink
|
||||
* @return URL for offramp service or null if not available
|
||||
*/
|
||||
fun getOfframpUrl(
|
||||
|
|
@ -28,16 +28,19 @@ interface OfframpRepository {
|
|||
|
||||
/**
|
||||
* Registers a new app-initiated sell for [userWalletId] / [currencyId], prunes expired records, and returns a
|
||||
* fresh single-use `request_id` to embed in the provider redirect URL.
|
||||
* fresh `request_id` to embed in the provider redirect URL. The record stays valid until it expires.
|
||||
*/
|
||||
suspend fun registerPendingOfframp(userWalletId: UserWalletId, currencyId: String): String
|
||||
|
||||
/**
|
||||
* Returns and removes (single-use) the pending sell matching [requestId] only when it is not expired and was
|
||||
* registered for the same [userWalletId] and [currencyId]. Returns `null` otherwise, leaving a non-matching
|
||||
* record untouched so a tampered redirect cannot burn a legitimate pending sell.
|
||||
* Returns the pending sell matching [requestId] when it is not expired and was registered for the same
|
||||
* [userWalletId] and [currencyId]; returns `null` otherwise.
|
||||
*
|
||||
* The matching record is **not** removed — it remains valid until it expires, so the same `redirect_sell`
|
||||
* deeplink can be followed repeatedly within that window (e.g. the user re-opens it). Expired records are pruned
|
||||
* as a side effect.
|
||||
*/
|
||||
suspend fun consumePendingOfframp(
|
||||
suspend fun resolvePendingOfframp(
|
||||
requestId: String,
|
||||
userWalletId: UserWalletId,
|
||||
currencyId: String,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
|||
import com.arkivanov.decompose.router.stack.backStack
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.tangem.common.ui.markets.action.TokenActionsContext
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
|
|
@ -64,6 +65,7 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
|
|||
callbacks = model,
|
||||
data = model.tokenActionsData,
|
||||
bottomAction = flowOf(BottomAction.GoToToken),
|
||||
context = TokenActionsContext.AddFunds,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,14 @@ internal enum class SwapMarketCategory(
|
|||
title = resourceReference(R.string.markets_sort_by_top_losers_title),
|
||||
order = TokenMarketListConfig.Order.TopLosers,
|
||||
),
|
||||
ExperiencedBuyers(
|
||||
title = resourceReference(R.string.markets_sort_by_experienced_buyers_title),
|
||||
order = TokenMarketListConfig.Order.Buyers,
|
||||
),
|
||||
Trending(
|
||||
title = resourceReference(R.string.markets_sort_by_trending_title),
|
||||
order = TokenMarketListConfig.Order.Trending,
|
||||
),
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -9,13 +9,9 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.commonfeatures.api.R
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenAnalyticsPayload
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooserBlock
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.*
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
|
||||
import com.tangem.features.commonfeatures.impl.choosetoken.AddToPortfolioRoute
|
||||
import com.tangem.features.commonfeatures.impl.choosetoken.converter.SearchBarToggleTransformer
|
||||
import com.tangem.features.commonfeatures.impl.choosetoken.converter.SearchBarUpdateQueryTransformer
|
||||
|
|
@ -24,7 +20,9 @@ import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenFullUM
|
|||
import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenInitialUM
|
||||
import com.tangem.features.commonfeatures.impl.choosetoken.ui.state.ChooserBlockUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -106,6 +104,13 @@ internal class ChooseTokenModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
if (bridge.settings.chooserBlock == ChooserBlock.Market) {
|
||||
modelScope.launch {
|
||||
delay(MARKETS_INITIAL_LOAD_DELAY)
|
||||
marketBlockDelegate.loadDefaultMarkets()
|
||||
}
|
||||
}
|
||||
|
||||
addToPortfolioManager.onDismiss.receiveAsFlow()
|
||||
.onEach { bottomSheetNavigation.dismiss() }
|
||||
.launchIn(modelScope)
|
||||
|
|
@ -159,5 +164,8 @@ internal class ChooseTokenModel @Inject constructor(
|
|||
|
||||
companion object {
|
||||
const val DEBOUNCE_SEARCH_DELAY = 500L
|
||||
|
||||
/** Roughly the bottom sheet entrance animation duration. */
|
||||
private const val MARKETS_INITIAL_LOAD_DELAY = 400L
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,8 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
private const val MARKET_PULSE_ITEM_LIMIT = 5
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class MarketBlockDelegate @AssistedInject constructor(
|
||||
private val marketsListBatchFlowManagerFactory: MarketsListBatchFlowManager.Factory,
|
||||
|
|
@ -77,7 +79,7 @@ internal class MarketBlockDelegate @AssistedInject constructor(
|
|||
* When single-currency wallets aren't selectable here (e.g. swap), the wallet is always
|
||||
* multi-currency, so we skip the per-wallet logic entirely and return [baseMarketsStateFlow].
|
||||
*/
|
||||
val marketsStateFlow: Flow<SwapMarketState?> = if (!shouldShowSingleCurrencyWallets) {
|
||||
private val walletAwareMarketsStateFlow: Flow<SwapMarketState?> = if (!shouldShowSingleCurrencyWallets) {
|
||||
baseMarketsStateFlow
|
||||
} else {
|
||||
selectedWalletFlow
|
||||
|
|
@ -85,6 +87,10 @@ internal class MarketBlockDelegate @AssistedInject constructor(
|
|||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
val marketsStateFlow: Flow<SwapMarketState?> = walletAwareMarketsStateFlow
|
||||
.map { it.limitMarketPulseItems() }
|
||||
.distinctUntilChanged()
|
||||
|
||||
private val defaultMarketsListManager by lazy {
|
||||
marketsListBatchFlowManagerFactory.create(
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main,
|
||||
|
|
@ -113,9 +119,6 @@ internal class MarketBlockDelegate @AssistedInject constructor(
|
|||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
// Initial load of default markets
|
||||
defaultMarketsListManager.reload()
|
||||
|
||||
visibleMarketItemIds
|
||||
.mapNotNull { rawIDS ->
|
||||
if (rawIDS.isNotEmpty()) {
|
||||
|
|
@ -144,14 +147,22 @@ internal class MarketBlockDelegate @AssistedInject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the initial load of the default markets list. Not invoked in [init] on purpose:
|
||||
* the caller decides *if* (market block may be disabled entirely, e.g. Transfer flow) and
|
||||
* *when* (deferred past the bottom sheet entrance animation, [REDACTED_TASK_KEY]) to trigger it.
|
||||
*/
|
||||
fun loadDefaultMarkets() {
|
||||
defaultMarketsListManager.reload()
|
||||
}
|
||||
|
||||
private fun createDefaultMarketsFlow(): Flow<SwapMarketState> {
|
||||
val marketsTitle = TextReference.Res(R.string.markets_pulse_common_title)
|
||||
return combine(
|
||||
flow = defaultMarketsListManager.uiItems,
|
||||
flow2 = defaultMarketsListManager.isInInitialLoadingErrorState,
|
||||
flow3 = defaultMarketsListManager.totalCount,
|
||||
flow4 = selectedCategoryFlow,
|
||||
) { uiItems, isError, total, selectedCategory ->
|
||||
flow3 = selectedCategoryFlow,
|
||||
) { uiItems, isError, selectedCategory ->
|
||||
val categories = buildCategoriesUM(selectedCategory)
|
||||
when {
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
|
|
@ -167,10 +178,10 @@ internal class MarketBlockDelegate @AssistedInject constructor(
|
|||
)
|
||||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { defaultMarketsListManager.loadMore() },
|
||||
loadMore = {},
|
||||
onItemClick = { item -> addToPortfolioItem(item) },
|
||||
visibleIdsChanged = { visibleDefaultMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
total = uiItems.size,
|
||||
marketsTitle = marketsTitle,
|
||||
shouldAssetsCount = false,
|
||||
categories = categories,
|
||||
|
|
@ -258,6 +269,13 @@ internal class MarketBlockDelegate @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun SwapMarketState?.limitMarketPulseItems(): SwapMarketState? {
|
||||
if (this !is SwapMarketState.Content || shouldAssetsCount) return this
|
||||
if (items.size <= MARKET_PULSE_ITEM_LIMIT) return this
|
||||
val limitedItems = items.take(MARKET_PULSE_ITEM_LIMIT).toImmutableList()
|
||||
return copy(items = limitedItems, total = limitedItems.size)
|
||||
}
|
||||
|
||||
private fun addToPortfolioItem(item: MarketsListItemUM) {
|
||||
val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id)
|
||||
?: searchMarketsListManager.getTokenMarketById(item.id) ?: return
|
||||
|
|
|
|||
|
|
@ -19,12 +19,14 @@ import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
|
|||
import com.tangem.features.commonfeatures.api.choosetoken.model.TokenListUMData
|
||||
import com.tangem.features.commonfeatures.impl.choosetoken.SettingContextUseCase
|
||||
import com.tangem.features.commonfeatures.impl.choosetoken.converter.ChooseTokenListItemConverter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.mapNotNullValues
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -33,6 +35,7 @@ internal class PortfolioListBlockDelegate @AssistedInject constructor(
|
|||
private val settingContext: SettingContextUseCase,
|
||||
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
@Assisted private val modelScope: CoroutineScope,
|
||||
@Assisted private val searchQueryState: StateFlow<SearchQuery>,
|
||||
@Assisted private val featureSettings: ChooseTokenBridge.Settings,
|
||||
|
|
@ -48,6 +51,8 @@ internal class PortfolioListBlockDelegate @AssistedInject constructor(
|
|||
|
||||
val portfolioList: SharedFlow<Map<UserWalletId, TokenListUMData>> = buildDataFlow()
|
||||
.distinctUntilChanged()
|
||||
.throttleLatest(windowMs = UM_UPDATES_THROTTLE_MS)
|
||||
.flowOn(dispatchers.default)
|
||||
.shareIn(modelScope, SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
private fun buildDataFlow(): Flow<Map<UserWalletId, TokenListUMData>> = channelFlow {
|
||||
|
|
@ -153,4 +158,12 @@ internal interface ClickIntents {
|
|||
fun onAccountExpandClick(account: Account)
|
||||
|
||||
fun onAccountCollapseClick(account: Account)
|
||||
}
|
||||
|
||||
private const val UM_UPDATES_THROTTLE_MS = 100L
|
||||
|
||||
/** Emits the first value immediately, then at most one (latest) value per [windowMs]. */
|
||||
private fun <T> Flow<T>.throttleLatest(windowMs: Long): Flow<T> = conflate().transform { value ->
|
||||
emit(value)
|
||||
delay(windowMs)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.commonfeatures.impl.choosetoken.ui
|
|||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.BoundsTransform
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.animateIntAsState
|
||||
|
|
@ -97,6 +98,7 @@ import com.tangem.utils.StringsSigns.DOT
|
|||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlin.random.Random
|
||||
|
||||
private const val LOAD_MORE_BUFFER = 25
|
||||
|
|
@ -173,7 +175,7 @@ private fun Content(state: ChooseTokenFullUM, modifier: Modifier = Modifier) {
|
|||
val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection()
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
TangemSharedTransitionLayout(modifier) {
|
||||
Box(modifier) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -249,10 +251,13 @@ private fun SetupMarketScrollTracker(marketsState: SwapMarketState, lazyListStat
|
|||
|
||||
@Composable
|
||||
private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapMarketState.Content) {
|
||||
val visibleItems by remember {
|
||||
val keyToId = remember(marketState.items) {
|
||||
marketState.items.associateBy({ it.getComposeKey() }, { it.id })
|
||||
}
|
||||
val visibleItems by remember(keyToId) {
|
||||
derivedStateOf {
|
||||
lazyListState.layoutInfo.visibleItemsInfo.mapNotNull { itemInfo ->
|
||||
marketState.items.find { it.getComposeKey() == itemInfo.key }?.id
|
||||
(itemInfo.key as? String)?.let(keyToId::get)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -282,14 +287,36 @@ private fun LazyListScope.assetsTitle() {
|
|||
private fun LazyListScope.walletListItem(walletList: WalletListUM) {
|
||||
if (walletList.items.isEmpty()) return
|
||||
item("wallet_list") {
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
) {
|
||||
items(walletList.items) { um ->
|
||||
WalletTabItem(um)
|
||||
}
|
||||
WalletList(walletList)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletList(walletList: WalletListUM, modifier: Modifier = Modifier) {
|
||||
val listState = rememberLazyListState()
|
||||
val selectedIndex = walletList.items.indexOfFirst { it.isSelected }
|
||||
|
||||
LaunchedEffect(selectedIndex) {
|
||||
if (selectedIndex < 0) return@LaunchedEffect
|
||||
val layoutInfo = snapshotFlow { listState.layoutInfo }
|
||||
.first { it.visibleItemsInfo.isNotEmpty() }
|
||||
val selectedItem = layoutInfo.visibleItemsInfo.firstOrNull { it.index == selectedIndex }
|
||||
val isFullyVisible = selectedItem != null &&
|
||||
selectedItem.offset >= layoutInfo.viewportStartOffset &&
|
||||
selectedItem.offset + selectedItem.size <= layoutInfo.viewportEndOffset
|
||||
if (!isFullyVisible) {
|
||||
listState.scrollToItem(selectedIndex)
|
||||
}
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
state = listState,
|
||||
modifier = modifier.padding(top = 16.dp, bottom = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
) {
|
||||
items(walletList.items) { um ->
|
||||
WalletTabItem(um)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -497,7 +524,7 @@ private fun AccountRow(
|
|||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ProvideSharedTransitionScope(Modifier.weight(1f)) {
|
||||
AccountRowSharedTransitionLayout(Modifier.weight(1f)) {
|
||||
val iconSharedContentState = rememberSharedContentState(key = "account-icon-${portfolio.id}")
|
||||
val titleSharedContentState = rememberSharedContentState(key = "account-title-${portfolio.id}")
|
||||
val boundsTransform = BoundsTransform { _, _ -> tween(ACCOUNT_BOUNDS_ANIM_MS) }
|
||||
|
|
@ -549,9 +576,7 @@ private fun AccountRow(
|
|||
)
|
||||
val startStyle = TangemTheme.typography2.captionSemibold12
|
||||
val stopStyle = TangemTheme.typography2.bodySemibold16
|
||||
val textStyle by remember(animationFraction.value) {
|
||||
derivedStateOf { lerp(startStyle, stopStyle, animationFraction.value) }
|
||||
}
|
||||
val textStyle = lerp(startStyle, stopStyle, animationFraction.value)
|
||||
val resizedTitle = when (val titleUM = tokenRowUM.titleUM) {
|
||||
is TangemTokenRowUM.TitleUM.Content -> titleUM.copy(
|
||||
text = styledStringReference(
|
||||
|
|
@ -600,6 +625,17 @@ private fun AccountRow(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
private fun AccountRowSharedTransitionLayout(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable SharedTransitionScope.() -> Unit,
|
||||
) {
|
||||
TangemSharedTransitionLayout(modifier) {
|
||||
ProvideSharedTransitionScope(content = content)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountTail(isExpanded: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.common.routing.AppRouter
|
|||
import com.tangem.common.ui.markets.action.CryptoCurrencyData
|
||||
import com.tangem.common.ui.markets.action.TokenActionsBSContentUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.TransferAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
|
|
@ -23,6 +24,7 @@ import com.tangem.domain.models.account.filterCryptoPortfolio
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
|
||||
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData
|
||||
|
|
@ -153,12 +155,7 @@ internal class ManageFundsModel @Inject constructor(
|
|||
|
||||
override fun onQuickActionClick(action: TokenActionsBSContentUM.Action, shouldDismiss: Boolean) {
|
||||
val event = when (flowType) {
|
||||
ManageFundsComponent.FlowType.AddFunds -> when (action) {
|
||||
TokenActionsBSContentUM.Action.Buy -> ManageFundsAnalyticsEvent.ButtonBuy()
|
||||
TokenActionsBSContentUM.Action.Exchange -> ManageFundsAnalyticsEvent.ButtonSwap()
|
||||
TokenActionsBSContentUM.Action.Receive -> ManageFundsAnalyticsEvent.ButtonReceive()
|
||||
else -> null
|
||||
}
|
||||
ManageFundsComponent.FlowType.AddFunds -> addFundsQuickActionEvent(action)
|
||||
ManageFundsComponent.FlowType.Transfer -> when (action) {
|
||||
TokenActionsBSContentUM.Action.Send -> TransferAnalyticsEvent.ButtonSend()
|
||||
TokenActionsBSContentUM.Action.Exchange -> TransferAnalyticsEvent.ButtonSwap()
|
||||
|
|
@ -173,6 +170,51 @@ internal class ManageFundsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun addFundsQuickActionEvent(action: TokenActionsBSContentUM.Action): AnalyticsEvent? {
|
||||
val request = tokenActionsTrigger.value
|
||||
return if (launchMode is ManageFundsComponent.LaunchMode.TokenActionsOnly && request != null) {
|
||||
tokenScreenQuickActionEvent(action, request)
|
||||
} else {
|
||||
when (action) {
|
||||
TokenActionsBSContentUM.Action.Buy -> ManageFundsAnalyticsEvent.ButtonBuy()
|
||||
TokenActionsBSContentUM.Action.Exchange -> ManageFundsAnalyticsEvent.ButtonSwap()
|
||||
TokenActionsBSContentUM.Action.Receive -> ManageFundsAnalyticsEvent.ButtonReceive()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun tokenScreenQuickActionEvent(
|
||||
action: TokenActionsBSContentUM.Action,
|
||||
request: TokenActionsRequest,
|
||||
): AnalyticsEvent? {
|
||||
val token = request.status.currency.symbol
|
||||
val blockchain = request.status.currency.network.name
|
||||
val derivationIndex = request.account.account
|
||||
.takeUnless { it.isMainAccount }
|
||||
?.derivationIndex?.value
|
||||
return when (action) {
|
||||
TokenActionsBSContentUM.Action.Buy -> TokenScreenAnalyticsEvent.ButtonWithParams.ButtonBuy(
|
||||
token = token,
|
||||
blockchain = blockchain,
|
||||
status = TokenScreenAnalyticsEvent.AVAILABLE,
|
||||
derivationIndex = derivationIndex,
|
||||
)
|
||||
TokenActionsBSContentUM.Action.Exchange -> TokenScreenAnalyticsEvent.ButtonWithParams.ButtonExchange(
|
||||
token = token,
|
||||
status = TokenScreenAnalyticsEvent.AVAILABLE,
|
||||
blockchain = blockchain,
|
||||
derivationIndex = derivationIndex,
|
||||
)
|
||||
TokenActionsBSContentUM.Action.Receive -> TokenScreenAnalyticsEvent.ButtonWithParams.ButtonReceive(
|
||||
token = token,
|
||||
status = TokenScreenAnalyticsEvent.AVAILABLE,
|
||||
blockchain = blockchain,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun buildAvailableToAddDataForChooser(): AvailableToAddData {
|
||||
val byWallet = filteredEntries.value.groupBy { it.userWallet.walletId }
|
||||
return AvailableToAddData(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import io.mockk.every
|
|||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -115,6 +116,23 @@ internal class MarketBlockDelegateTest {
|
|||
assertThat((result as SwapMarketState.Content).items).containsExactly(item1, item2).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN more than 5 market items WHEN default flow emitted THEN only first 5 shown`() = runTest {
|
||||
// Arrange
|
||||
val items = (1..7).map { marketItem("token-$it") }
|
||||
defaultUiItems.value = items.toPersistentList()
|
||||
val delegate = createDelegate(wallet = MockUserWalletFactory.create())
|
||||
|
||||
// Act
|
||||
val result = lastMarketState(delegate)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isInstanceOf(SwapMarketState.Content::class.java)
|
||||
val content = result as SwapMarketState.Content
|
||||
assertThat(content.items).containsExactlyElementsIn(items.take(5)).inOrder()
|
||||
assertThat(content.total).isEqualTo(5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN single-currency wallet WHEN trending emitted THEN market block is hidden`() = runTest {
|
||||
// Arrange
|
||||
|
|
@ -187,6 +205,28 @@ internal class MarketBlockDelegateTest {
|
|||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN NODL wallet WHEN network token is beyond first 5 THEN block still shows it`() = runTest {
|
||||
// Arrange — 5 tokens on another network first, the wallet-network token only at position 6.
|
||||
val nodlWallet = MockUserWalletFactory.createSingleWalletWithToken()
|
||||
val otherNetworkItems = (1..5).map { marketItem("token-eth-$it") }
|
||||
val walletNetworkItem = marketItem("token-stellar")
|
||||
(1..5).forEach { tokenMarketsByRawId["token-eth-$it"] = tokenMarket(ETHEREUM_NETWORK_ID) }
|
||||
tokenMarketsByRawId["token-stellar"] = tokenMarket(STELLAR_NETWORK_ID)
|
||||
defaultUiItems.value = (otherNetworkItems + walletNetworkItem).toPersistentList()
|
||||
|
||||
every {
|
||||
singleAccountStatusListSupplier(nodlWallet.walletId)
|
||||
} returns flowOf(accountStatusList(STELLAR_NETWORK_ID))
|
||||
|
||||
// Act
|
||||
val result = lastMarketState(createDelegate(wallet = nodlWallet))
|
||||
|
||||
// Assert — network filtering runs on the full list before the 5-item cap, so it isn't dropped.
|
||||
assertThat(result).isInstanceOf(SwapMarketState.Content::class.java)
|
||||
assertThat((result as SwapMarketState.Content).items).containsExactly(walletNetworkItem)
|
||||
}
|
||||
|
||||
// region Helpers
|
||||
|
||||
private fun TestScope.lastMarketState(delegate: MarketBlockDelegate): SwapMarketState? {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,11 @@ internal class DefaultFeedComponent(
|
|||
modifier = modifier,
|
||||
state = state,
|
||||
promoBannersBlockComponent = ComposableContentComponent { promoModifier ->
|
||||
promoBannersBlockComponent.ContentWithPadding(modifier = promoModifier, horizontalItemPadding = 16.dp)
|
||||
promoBannersBlockComponent.ContentWithPadding(
|
||||
modifier = promoModifier,
|
||||
horizontalItemPadding = 16.dp,
|
||||
walletId = null,
|
||||
)
|
||||
},
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -141,13 +141,16 @@ private fun LazyListScope.searchHistoryItems(
|
|||
onHintClick: (String) -> Unit,
|
||||
onHistoryTokenClick: (MarketsListItemUM) -> Unit,
|
||||
) {
|
||||
if (!history.textHints.isEmpty() || !history.recentTokens.isEmpty()) {
|
||||
item(key = "recents") {
|
||||
if (!history.textHints.isEmpty()) {
|
||||
item(key = "recent searches") {
|
||||
SectionHeader(
|
||||
title = stringResourceSafe(R.string.markets_search_hint_header),
|
||||
onClearAllClick = onClearAllClick,
|
||||
)
|
||||
}
|
||||
item {
|
||||
SpacerH(12.dp)
|
||||
}
|
||||
}
|
||||
itemsIndexed(
|
||||
items = history.textHints,
|
||||
|
|
@ -162,7 +165,15 @@ private fun LazyListScope.searchHistoryItems(
|
|||
}
|
||||
}
|
||||
item {
|
||||
SpacerH(TangemTheme.dimens2.x2)
|
||||
SpacerH(24.dp)
|
||||
}
|
||||
if (!history.recentTokens.isEmpty()) {
|
||||
item(key = "recent tokens") {
|
||||
SectionHeader(title = stringResourceSafe(R.string.markets_search_recent_tokens_header))
|
||||
}
|
||||
item {
|
||||
SpacerH(12.dp)
|
||||
}
|
||||
}
|
||||
items(
|
||||
items = history.recentTokens,
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import com.tangem.core.ui.extensions.conditional
|
|||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.features.foryou.impl.model.ForYouNotification
|
||||
import com.tangem.features.foryou.impl.entity.ForYouUM
|
||||
import com.tangem.features.foryou.impl.model.ForYouNotification
|
||||
import com.tangem.features.foryou.impl.ui.preview.ForYouPortfolioReviewPreviewData
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -55,6 +55,7 @@ internal fun ForYouContent(
|
|||
) {
|
||||
promoBannersBlockComponent.ContentWithPadding(
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
walletId = null,
|
||||
horizontalItemPadding = 16.dp,
|
||||
)
|
||||
|
||||
|
|
@ -94,7 +95,7 @@ private fun ForYouContent_Preview(@PreviewParameter(ForYouContentPreviewProvider
|
|||
bottomSheetState = remember { mutableStateOf(BottomSheetState.EXPANDED) },
|
||||
promoBannersBlockComponent = object : PromoBannersBlockComponent {
|
||||
@Composable
|
||||
override fun ContentWithPadding(horizontalItemPadding: Dp, modifier: Modifier) {
|
||||
override fun ContentWithPadding(horizontalItemPadding: Dp, walletId: String?, modifier: Modifier) {
|
||||
}
|
||||
|
||||
override fun setVisibleOnScreen(isVisible: Boolean) {}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ internal class AllOffersModel @Inject constructor(
|
|||
analyticsEventHandler.send(
|
||||
event = OnrampAnalyticsEvent.OnPaymentMethodChosen(paymentMethod = method.methodConfig.method.name),
|
||||
)
|
||||
analyticsEventHandler.send(OnrampAnalyticsEvent.ProvidersScreenOpened())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
package com.tangem.features.promobanners.api
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
|
||||
@Immutable
|
||||
interface PromoBannersBlockComponent {
|
||||
|
||||
/**
|
||||
* @param walletId when non-null, renders the banners of that specific wallet (used by the wallet
|
||||
* pager so each page shows its own banners synchronously while swiping); when null, renders the
|
||||
* currently selected wallet's banners.
|
||||
*/
|
||||
@Composable
|
||||
fun ContentWithPadding(horizontalItemPadding: Dp, modifier: Modifier)
|
||||
fun ContentWithPadding(horizontalItemPadding: Dp, walletId: String?, modifier: Modifier)
|
||||
|
||||
fun setVisibleOnScreen(isVisible: Boolean)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,15 @@ internal class DefaultPromoBannersBlockComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
@Composable
|
||||
override fun ContentWithPadding(horizontalItemPadding: Dp, modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
override fun ContentWithPadding(horizontalItemPadding: Dp, walletId: String?, modifier: Modifier) {
|
||||
val state = if (walletId != null) {
|
||||
val statesByWallet by model.bannerStates.collectAsStateWithLifecycle()
|
||||
statesByWallet[walletId]
|
||||
} else {
|
||||
val selectedState by model.uiState.collectAsStateWithLifecycle()
|
||||
selectedState
|
||||
} ?: return
|
||||
|
||||
PromoBannersBlock(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package com.tangem.features.promobanners.impl.model
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.deeplink.DeeplinkLauncher
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
import com.tangem.features.promobanners.impl.analytics.PromoBannerAnalyticsEvent
|
||||
|
|
@ -13,11 +13,13 @@ import com.tangem.features.promobanners.impl.converters.PromoBannerDisplayToNoti
|
|||
import com.tangem.features.promobanners.impl.repository.PromoBannersRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
|
@ -39,20 +41,58 @@ internal class PromoBannersBlockModel @Inject constructor(
|
|||
|
||||
private val placeholderName: String = params.placeholder.value
|
||||
private val shownBannerIds: MutableSet<ShownBannerKey> = ConcurrentHashMap.newKeySet()
|
||||
private var isVisibleOnScreen: Boolean = params.isInitiallyVisibleOnScreen
|
||||
private var wasCarouselScrolled = false
|
||||
private val savedDisplayIdByWalletId: MutableMap<String, Int> = mutableMapOf()
|
||||
private val prefetchedWalletIds: MutableSet<String> = ConcurrentHashMap.newKeySet()
|
||||
private val prefetchSemaphore = Semaphore(permits = PREFETCH_PARALLELISM)
|
||||
|
||||
val uiState: StateFlow<PromoBannersBlockUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
/** Raw banners per wallet, filled from cache/prefetch/network. Source of truth for the UI. */
|
||||
private val rawBannersByWalletId = MutableStateFlow<Map<String, List<PromoBannerDisplay>>>(emptyMap())
|
||||
|
||||
private val selectedWalletId = MutableStateFlow<String?>(null)
|
||||
|
||||
private val screenVisible = MutableStateFlow(params.isInitiallyVisibleOnScreen)
|
||||
|
||||
private val baseBannerStates: StateFlow<Map<String, PromoBannersBlockUM>> =
|
||||
rawBannersByWalletId
|
||||
.map { rawByWallet ->
|
||||
rawByWallet.mapValues { (walletId, banners) -> buildState(walletId, banners, isVisible = false) }
|
||||
}
|
||||
.stateIn(modelScope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
/**
|
||||
* Per-wallet UI states, so a pager page can render its own wallet's banners synchronously from
|
||||
* cache as it slides in — instead of the whole block waiting for the wallet selection to settle.
|
||||
* Only cheaply toggles [PromoBannersBlockUM.isVisibleOnScreen] on selection/visibility change.
|
||||
*/
|
||||
val bannerStates: StateFlow<Map<String, PromoBannersBlockUM>> =
|
||||
combine(baseBannerStates, selectedWalletId, screenVisible) { base, selected, visible ->
|
||||
base.mapValues { (walletId, state) ->
|
||||
// Only the active wallet is "visible on screen" for analytics purposes; pre-composed
|
||||
// off-screen pager pages must not emit "banner shown" events.
|
||||
val shouldBeVisible = visible && walletId == selected
|
||||
if (state.isVisibleOnScreen == shouldBeVisible) {
|
||||
state
|
||||
} else {
|
||||
state.copy(
|
||||
isVisibleOnScreen = shouldBeVisible,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.stateIn(modelScope, SharingStarted.Eagerly, emptyMap())
|
||||
|
||||
val uiState: StateFlow<PromoBannersBlockUM> =
|
||||
combine(bannerStates, selectedWalletId) { states, selected ->
|
||||
states[selected] ?: getInitialState()
|
||||
}.stateIn(modelScope, SharingStarted.Eagerly, getInitialState())
|
||||
|
||||
init {
|
||||
subscribeOnSelectedWallet()
|
||||
prefetchAllWallets()
|
||||
}
|
||||
|
||||
fun setVisibleOnScreen(visible: Boolean) {
|
||||
isVisibleOnScreen = visible
|
||||
uiState.update { it.copy(isVisibleOnScreen = visible) }
|
||||
screenVisible.value = visible
|
||||
}
|
||||
|
||||
private fun subscribeOnSelectedWallet() {
|
||||
|
|
@ -61,50 +101,97 @@ internal class PromoBannersBlockModel @Inject constructor(
|
|||
.filterNotNull()
|
||||
.map { it.walletId.stringValue }
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
.collectLatest { walletId ->
|
||||
wasCarouselScrolled = false
|
||||
selectedWalletId.value = walletId
|
||||
loadWallet(walletId)
|
||||
}
|
||||
.collectLatest { walletId -> loadBanners(walletId) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadBanners(walletId: String) {
|
||||
val languageISOCode = Locale.getDefault().language
|
||||
/**
|
||||
* Warms the banners for every wallet in the background so switching to another wallet renders its
|
||||
* banners instantly from cache instead of after a network gap. Each wallet is fetched at most once
|
||||
* ([prefetchedWalletIds]); [PromoBannersRepository.getBanners] is a no-op once the wallet is cached.
|
||||
*/
|
||||
private fun prefetchAllWallets() {
|
||||
modelScope.launch {
|
||||
val languageISOCode = Locale.getDefault().language
|
||||
userWalletsListRepository.userWallets
|
||||
.filterNotNull()
|
||||
.collect { wallets ->
|
||||
wallets.forEach { wallet ->
|
||||
val walletId = wallet.walletId.stringValue
|
||||
if (prefetchedWalletIds.add(walletId)) {
|
||||
modelScope.launch {
|
||||
prefetchSemaphore.withPermit {
|
||||
runSuspendCatching {
|
||||
repository.getBanners(walletId, params.placeholder, languageISOCode)
|
||||
}.onSuccess { putBanners(walletId, it) }
|
||||
.onFailure { prefetchedWalletIds.remove(walletId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadWallet(walletId: String) {
|
||||
val cached = runSuspendCatching { repository.getCachedBanners(walletId, params.placeholder) }.getOrNull()
|
||||
if (cached != null) {
|
||||
putBanners(walletId, cached)
|
||||
return
|
||||
}
|
||||
|
||||
putBanners(walletId, banners = emptyList())
|
||||
runSuspendCatching {
|
||||
repository.getBanners(walletId, params.placeholder, languageISOCode)
|
||||
repository.getBanners(walletId, params.placeholder, Locale.getDefault().language)
|
||||
}.onSuccess { banners ->
|
||||
val bannerUMs = banners.map { banner ->
|
||||
converter.convert(
|
||||
banner = banner,
|
||||
onDeeplinkClick = { deeplink -> onButtonClick(banner.id, deeplink) },
|
||||
onDismiss = { displayId -> onBannerDismiss(walletId, displayId) },
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
val savedDisplayId = savedDisplayIdByWalletId[walletId]
|
||||
val initialPage = if (savedDisplayId != null) {
|
||||
bannerUMs.indexOfFirst { it.displayId == savedDisplayId }.coerceAtLeast(0)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
uiState.value = PromoBannersBlockUM(
|
||||
userWalletId = walletId,
|
||||
initialPage = initialPage,
|
||||
banners = bannerUMs,
|
||||
isVisibleOnScreen = isVisibleOnScreen,
|
||||
placeholder = params.placeholder,
|
||||
onBannerShown = { displayId -> onBannerShown(walletId, displayId) },
|
||||
onCarouselScrolled = ::onCarouselScrolled,
|
||||
onPageChanged = { displayId -> savedDisplayIdByWalletId[walletId] = displayId },
|
||||
)
|
||||
putBanners(walletId, banners)
|
||||
}.onFailure { error ->
|
||||
TangemLogger.w("Failed to load promo banners", error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun putBanners(walletId: String, banners: List<PromoBannerDisplay>) {
|
||||
rawBannersByWalletId.update { it + (walletId to banners) }
|
||||
}
|
||||
|
||||
private fun buildState(
|
||||
walletId: String,
|
||||
banners: List<PromoBannerDisplay>,
|
||||
isVisible: Boolean,
|
||||
): PromoBannersBlockUM {
|
||||
val bannerUMs = banners.map { banner ->
|
||||
converter.convert(
|
||||
banner = banner,
|
||||
onDeeplinkClick = { deeplink -> onButtonClick(banner.id, deeplink) },
|
||||
onDismiss = { displayId -> onBannerDismiss(walletId, displayId) },
|
||||
)
|
||||
}.toImmutableList()
|
||||
|
||||
val savedDisplayId = savedDisplayIdByWalletId[walletId]
|
||||
val initialPage = if (savedDisplayId != null) {
|
||||
bannerUMs.indexOfFirst { it.displayId == savedDisplayId }.coerceAtLeast(0)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
return PromoBannersBlockUM(
|
||||
userWalletId = walletId,
|
||||
initialPage = initialPage,
|
||||
banners = bannerUMs,
|
||||
isVisibleOnScreen = isVisible,
|
||||
placeholder = params.placeholder,
|
||||
onBannerShown = { displayId -> onBannerShown(walletId, displayId) },
|
||||
onCarouselScrolled = ::onCarouselScrolled,
|
||||
onPageChanged = { displayId -> savedDisplayIdByWalletId[walletId] = displayId },
|
||||
)
|
||||
}
|
||||
|
||||
private fun onBannerShown(walletId: String, displayId: Int) {
|
||||
if (walletId != selectedWalletId.value) return
|
||||
if (shownBannerIds.add(walletId to displayId)) {
|
||||
analyticsEventHandler.send(PromoBannerAnalyticsEvent.Shown(displayId, placeholderName))
|
||||
}
|
||||
|
|
@ -137,7 +224,7 @@ internal class PromoBannersBlockModel @Inject constructor(
|
|||
userWalletId = "",
|
||||
initialPage = 0,
|
||||
banners = persistentListOf(),
|
||||
isVisibleOnScreen = isVisibleOnScreen,
|
||||
isVisibleOnScreen = screenVisible.value,
|
||||
placeholder = params.placeholder,
|
||||
onBannerShown = {},
|
||||
onCarouselScrolled = {},
|
||||
|
|
@ -146,12 +233,9 @@ internal class PromoBannersBlockModel @Inject constructor(
|
|||
|
||||
private fun onBannerDismiss(walletId: String, displayId: Int) {
|
||||
analyticsEventHandler.send(PromoBannerAnalyticsEvent.Dismissed(displayId, placeholderName))
|
||||
uiState.update { state ->
|
||||
state.copy(
|
||||
banners = state.banners
|
||||
.filterNot { it.displayId == displayId }
|
||||
.toImmutableList(),
|
||||
)
|
||||
rawBannersByWalletId.update { byWallet ->
|
||||
val banners = byWallet[walletId] ?: return@update byWallet
|
||||
byWallet + (walletId to banners.filterNot { it.id == displayId })
|
||||
}
|
||||
modelScope.launch {
|
||||
runSuspendCatching {
|
||||
|
|
@ -169,5 +253,6 @@ internal class PromoBannersBlockModel @Inject constructor(
|
|||
const val DEEPLINK_SCHEME_TANGEM = "tangem"
|
||||
const val DEEPLINK_HOST_SURVEY = "survey"
|
||||
const val QUERY_DISPLAY_ID = "display_id"
|
||||
const val PREFETCH_PARALLELISM = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,10 @@ internal class DefaultPromoBannersRepository(
|
|||
return banners
|
||||
}
|
||||
|
||||
override suspend fun getCachedBanners(walletId: String, placeholder: Placeholder): List<PromoBannerDisplay>? {
|
||||
return cache.getSyncOrNull()?.get(BannersCacheKey(walletId, placeholder))
|
||||
}
|
||||
|
||||
override suspend fun dismissBanner(walletId: String, displayId: Int) {
|
||||
cache.update(default = emptyMap()) { current ->
|
||||
current.mapValues { (key, banners) ->
|
||||
|
|
|
|||
|
|
@ -11,5 +11,7 @@ internal interface PromoBannersRepository {
|
|||
languageISOCode: String,
|
||||
): List<PromoBannerDisplay>
|
||||
|
||||
suspend fun getCachedBanners(walletId: String, placeholder: Placeholder): List<PromoBannerDisplay>?
|
||||
|
||||
suspend fun dismissBanner(walletId: String, displayId: Int)
|
||||
}
|
||||
|
|
@ -57,9 +57,11 @@ internal class DefaultSellRedirectDeepLinkHandler @AssistedInject constructor(
|
|||
|
||||
scope.launch {
|
||||
// Only trust the redirect if it carries a request_id we issued for a sell this
|
||||
// app actually started (single-use, bound to the wallet + currency). Otherwise an external
|
||||
// deeplink could inject a locked attacker recipient/amount into the Send confirm screen.
|
||||
val pendingOfframp = offrampRepository.consumePendingOfframp(
|
||||
// app actually started (bound to the wallet + currency, valid until it expires). Otherwise an
|
||||
// external deeplink could inject a locked attacker recipient/amount into the Send confirm
|
||||
// screen. The record is kept until expiry so the user can re-open the redirect within that
|
||||
// window.
|
||||
val pendingOfframp = offrampRepository.resolvePendingOfframp(
|
||||
requestId = requestId,
|
||||
userWalletId = userWallet.walletId,
|
||||
currencyId = currencyId,
|
||||
|
|
|
|||
|
|
@ -43,28 +43,43 @@ internal class DefaultSellRedirectDeepLinkHandlerTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN matching pending offramp WHEN deeplink handled THEN request passes the gate`() = runTest {
|
||||
coEvery { offrampRepository.consumePendingOfframp(requestId, userWalletId, currencyId) } returns pendingOfframp()
|
||||
coEvery { offrampRepository.resolvePendingOfframp(requestId, userWalletId, currencyId) } returns pendingOfframp()
|
||||
|
||||
createHandler(validParams())
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { offrampRepository.consumePendingOfframp(requestId, userWalletId, currencyId) }
|
||||
coVerify(exactly = 1) { offrampRepository.resolvePendingOfframp(requestId, userWalletId, currencyId) }
|
||||
coVerify(exactly = 1) { singleAccountListSupplier.getSyncOrNull(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN matching pending offramp WHEN deeplink handled twice THEN gate passes both times`() = runTest {
|
||||
// The pending sell is not single-use: resolving it does not remove it, so re-opening the same deeplink must
|
||||
// pass the gate again. Guards against reintroducing single-use behavior in the handler.
|
||||
coEvery { offrampRepository.resolvePendingOfframp(requestId, userWalletId, currencyId) } returns pendingOfframp()
|
||||
|
||||
createHandler(validParams())
|
||||
advanceUntilIdle()
|
||||
createHandler(validParams())
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 2) { offrampRepository.resolvePendingOfframp(requestId, userWalletId, currencyId) }
|
||||
coVerify(exactly = 2) { singleAccountListSupplier.getSyncOrNull(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no request_id WHEN deeplink handled THEN rejected without touching the store`() = runTest {
|
||||
createHandler(validParams() - REQUEST_ID_KEY)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { offrampRepository.consumePendingOfframp(any(), any(), any()) }
|
||||
coVerify(exactly = 0) { offrampRepository.resolvePendingOfframp(any(), any(), any()) }
|
||||
coVerify(exactly = 0) { singleAccountListSupplier.getSyncOrNull(any<UserWalletId>()) }
|
||||
verify(exactly = 0) { appRouter.push(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no matching pending offramp WHEN deeplink handled THEN rejected`() = runTest {
|
||||
coEvery { offrampRepository.consumePendingOfframp(requestId, userWalletId, currencyId) } returns null
|
||||
coEvery { offrampRepository.resolvePendingOfframp(requestId, userWalletId, currencyId) } returns null
|
||||
|
||||
createHandler(validParams())
|
||||
advanceUntilIdle()
|
||||
|
|
|
|||
|
|
@ -13,14 +13,13 @@ import androidx.compose.material3.CircularProgressIndicator
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.dropShadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.shadow.Shadow
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.testTag
|
||||
|
|
@ -31,6 +30,7 @@ import androidx.compose.ui.text.buildAnnotatedString
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import com.tangem.common.ui.footers.SendingText
|
||||
|
|
@ -305,7 +305,16 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) {
|
|||
Box(
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size48)
|
||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||
.dropShadow(
|
||||
shape = CircleShape,
|
||||
shadow = Shadow(
|
||||
radius = 6.dp,
|
||||
spread = 0.5.dp,
|
||||
color = Color.Black.copy(alpha = 0.1f),
|
||||
offset = DpOffset(1.dp, 1.dp),
|
||||
),
|
||||
)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(
|
||||
enabled = state.changeCardsButtonState == ChangeCardsButtonState.ENABLED,
|
||||
|
|
|
|||
|
|
@ -92,7 +92,11 @@ internal class TangemPayDetailsComponent(
|
|||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
val promoBannersBlock = ComposableContentComponent { promoModifier ->
|
||||
promoBannersBlockComponent.ContentWithPadding(modifier = promoModifier, horizontalItemPadding = 16.dp)
|
||||
promoBannersBlockComponent.ContentWithPadding(
|
||||
modifier = promoModifier,
|
||||
walletId = null,
|
||||
horizontalItemPadding = 16.dp,
|
||||
)
|
||||
}
|
||||
CompositionLocalProvider(LocalVisaRedesignEnabled provides model.isRedesignEnabled()) {
|
||||
NavigationBar3ButtonsScrim()
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import com.tangem.core.ui.utils.parseBigDecimal
|
|||
import com.tangem.domain.tokens.model.details.TokenAction
|
||||
import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent
|
||||
import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent
|
||||
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
|
||||
import com.tangem.feature.wallet.child.tokenActions.DefaultTokenActionsComponent
|
||||
import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent
|
||||
import com.tangem.feature.wallet.child.wallet.model.WalletModel
|
||||
|
|
@ -36,6 +35,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen2
|
|||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedComponent
|
||||
import com.tangem.feature.walletsettings.component.RenameWalletComponent
|
||||
import com.tangem.features.biometry.AskBiometryComponent
|
||||
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
|
||||
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
|
||||
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
|
|
@ -295,14 +295,10 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
var headerSize by remember { mutableStateOf(0.dp) }
|
||||
val dialog by dialog.subscribeAsState()
|
||||
val uiState by model.uiState.collectAsStateWithLifecycle()
|
||||
val promoBannersBlockComponentContentComponent = ComposableContentComponent { promoModifier ->
|
||||
promoBannersBlockComponent.ContentWithPadding(modifier = promoModifier, horizontalItemPadding = 12.dp)
|
||||
}
|
||||
|
||||
if (designFeatureToggles.isRedesignEnabled) {
|
||||
WalletScreen2(
|
||||
state = uiState,
|
||||
promoBannersBlockComponent = promoBannersBlockComponentContentComponent,
|
||||
promoBannersBlockComponent = promoBannersBlockComponent,
|
||||
tangemPayComponent = tangemPayMainBlockComponent,
|
||||
virtualAccountComponent = virtualAccountMainBlockComponent,
|
||||
bottomSheetContent = { onExpandSheet ->
|
||||
|
|
@ -319,7 +315,7 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
} else {
|
||||
WalletScreen(
|
||||
state = uiState,
|
||||
promoBannersBlockComponent = promoBannersBlockComponentContentComponent,
|
||||
promoBannersBlockComponent = promoBannersBlockComponent,
|
||||
tangemPayComponent = tangemPayMainBlockComponent,
|
||||
virtualAccountComponent = virtualAccountMainBlockComponent,
|
||||
bottomSheetContent = { onExpandSheet ->
|
||||
|
|
|
|||
|
|
@ -333,15 +333,19 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onNFTClick(userWallet: UserWallet) {
|
||||
val selectedWallet = stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content ?: return
|
||||
when (val state = selectedWallet.nftState) {
|
||||
val nftState = if (stateHolder.value.isRedesignEnabled) {
|
||||
stateHolder.getSelectedWalletUM().nftState
|
||||
} else {
|
||||
(stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content)?.nftState
|
||||
}
|
||||
when (nftState) {
|
||||
is WalletNFTItemUM.Content -> {
|
||||
analyticsEventHandler.send(
|
||||
NFTAnalyticsEvent.NFTListScreenOpened(
|
||||
state = AnalyticsParam.EmptyFull.Full,
|
||||
allAssetsCount = state.allAssetsCount,
|
||||
collectionsCount = state.collectionsCount,
|
||||
noCollectionAssetsCount = state.noCollectionAssetsCount,
|
||||
allAssetsCount = nftState.allAssetsCount,
|
||||
collectionsCount = nftState.collectionsCount,
|
||||
noCollectionAssetsCount = nftState.noCollectionAssetsCount,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -358,6 +362,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
|
|||
is WalletNFTItemUM.Failed,
|
||||
is WalletNFTItemUM.Hidden,
|
||||
is WalletNFTItemUM.Loading,
|
||||
null,
|
||||
-> Unit
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.tangem.core.ui.R as CoreUiR
|
|||
*/
|
||||
internal enum class WalletNotificationType {
|
||||
Status,
|
||||
AddFundsPromo,
|
||||
Critical,
|
||||
Warning,
|
||||
Promo,
|
||||
|
|
@ -367,7 +368,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
|
|||
),
|
||||
),
|
||||
),
|
||||
type = WalletNotificationType.Promo,
|
||||
type = WalletNotificationType.AddFundsPromo,
|
||||
)
|
||||
|
||||
data class NoteMigration(val onClick: () -> Unit) : WalletNotificationUM(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.subscribers
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.stories.GetStoryContentUseCase
|
||||
import com.tangem.domain.stories.models.StoryContentIds
|
||||
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
|
||||
|
|
@ -17,6 +19,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class WalletNotificationsSubscriber @AssistedInject constructor(
|
||||
|
|
@ -27,6 +30,7 @@ internal class WalletNotificationsSubscriber @AssistedInject constructor(
|
|||
private val getWalletNotificationsCarouselFactory: GetWalletNotificationsCarouselFactory,
|
||||
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
||||
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
|
||||
private val getStoryContentUseCase: GetStoryContentUseCase,
|
||||
) : WalletSubscriber() {
|
||||
|
||||
override fun create(coroutineScope: CoroutineScope): Flow<ImmutableList<WalletNotificationUM>> {
|
||||
|
|
@ -35,6 +39,15 @@ internal class WalletNotificationsSubscriber @AssistedInject constructor(
|
|||
flow2 = getWalletNotificationsCarouselFactory.create(userWallet, clickIntents).conflate()
|
||||
.distinctUntilChanged(),
|
||||
) { notifications, notificationsCarousel ->
|
||||
if (notificationsCarousel.any { it is WalletNotificationUM.YieldBoostPromo }) {
|
||||
coroutineScope.launch {
|
||||
getStoryContentUseCase.invokeSync(
|
||||
id = StoryContentIds.STORY_FIRST_TIME_YIELD_PROMO.id,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val displayedWalletUM = stateHolder.getWalletUM(userWallet.walletId)
|
||||
|
||||
// Wait until the wallet appears in the list
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ import com.tangem.core.ui.components.sheetscaffold.*
|
|||
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
|
||||
import com.tangem.core.ui.components.snackbar.TangemSnackbar
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.softLayerShadow
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
|
|
@ -83,6 +82,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
|
|||
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayMainUM
|
||||
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
|
||||
|
|
@ -98,7 +98,7 @@ internal fun WalletScreen(
|
|||
state: WalletScreenState,
|
||||
tangemPayComponent: TangemPayMainBlockComponent,
|
||||
virtualAccountComponent: VirtualAccountMainBlockComponent,
|
||||
promoBannersBlockComponent: ComposableContentComponent? = null,
|
||||
promoBannersBlockComponent: PromoBannersBlockComponent? = null,
|
||||
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
|
|
@ -142,7 +142,7 @@ private fun WalletContent(
|
|||
snackbarHostState: SnackbarHostState,
|
||||
isAutoScroll: State<Boolean>,
|
||||
onAutoScrollReset: () -> Unit,
|
||||
promoBannersBlockComponent: ComposableContentComponent? = null,
|
||||
promoBannersBlockComponent: PromoBannersBlockComponent? = null,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
|
||||
|
|
@ -226,7 +226,11 @@ private fun WalletContent(
|
|||
|
||||
promoBannersBlockComponent?.let { component ->
|
||||
item(key = "PromoBannersBlock") {
|
||||
component.Content(modifier = itemModifier)
|
||||
component.ContentWithPadding(
|
||||
horizontalItemPadding = 12.dp,
|
||||
modifier = itemModifier,
|
||||
walletId = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ import com.tangem.core.ui.components.haze.hazeEffectTangem
|
|||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.components.rememberIsKeyboardVisible
|
||||
import com.tangem.core.ui.components.sheetscaffold.*
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior
|
||||
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar
|
||||
import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior
|
||||
|
|
@ -74,12 +73,15 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.common.Wallet
|
|||
import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletPagerIndicator
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletTopBar
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.lazyListStateMapSaver
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayMainUM
|
||||
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
|
||||
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
import dev.chrisbanes.haze.HazeTint
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -95,7 +97,7 @@ internal fun WalletScreen2(
|
|||
tangemPayComponent: TangemPayMainBlockComponent,
|
||||
virtualAccountComponent: VirtualAccountMainBlockComponent,
|
||||
modifier: Modifier = Modifier,
|
||||
promoBannersBlockComponent: ComposableContentComponent? = null,
|
||||
promoBannersBlockComponent: PromoBannersBlockComponent? = null,
|
||||
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
|
|
@ -147,7 +149,7 @@ internal fun WalletScreen2(
|
|||
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
|
||||
onBottomSheetStateChange = onBottomSheetStateChange,
|
||||
modifier = modifier,
|
||||
listStates = listStates,
|
||||
listStates = remember(listStates) { listStates.toImmutableMap() },
|
||||
)
|
||||
|
||||
WalletEventEffect(
|
||||
|
|
@ -171,9 +173,9 @@ private fun WalletContent2(
|
|||
tangemPayComponent: TangemPayMainBlockComponent,
|
||||
virtualAccountComponent: VirtualAccountMainBlockComponent,
|
||||
behavior: TangemCollapsingAppBarBehavior,
|
||||
listStates: Map<Int, LazyListState>,
|
||||
listStates: ImmutableMap<Int, LazyListState>,
|
||||
modifier: Modifier = Modifier,
|
||||
promoBannersBlockComponent: ComposableContentComponent? = null,
|
||||
promoBannersBlockComponent: PromoBannersBlockComponent? = null,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
|
||||
|
|
@ -282,6 +284,7 @@ private fun WalletContent2(
|
|||
val currentWallet = state.wallets2.getOrElse(currentWalletIndex) {
|
||||
state.wallets2[state.selectedWalletIndex]
|
||||
}
|
||||
val currentWalletId = currentWallet.walletsBalanceUM.id.stringValue
|
||||
|
||||
LaunchedEffect(walletsPagerState.currentPage, currentWallet.walletsBalanceUM) {
|
||||
if (walletsPagerState.currentPage == currentWalletIndex) {
|
||||
|
|
@ -350,6 +353,7 @@ private fun WalletContent2(
|
|||
contentPadding = contentPadding,
|
||||
tangemPayComponent = tangemPayComponent,
|
||||
promoBannersBlockComponent = promoBannersBlockComponent,
|
||||
walletId = currentWalletId,
|
||||
virtualAccountComponent = virtualAccountComponent,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import com.tangem.common.ui.notifications.notifications
|
||||
import com.tangem.common.ui.notifications.notificationsCarousel
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.components.transactions.txHistoryItems
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.MainScreenTestTags
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
|
|
@ -26,6 +26,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.nftCollection
|
|||
import com.tangem.feature.wallet.presentation.wallet.ui.components.organizeTokens2
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.tangemPay
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.virtualAccount
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
|
||||
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
|
@ -40,7 +41,8 @@ internal fun WalletListContent(
|
|||
virtualAccountComponent: VirtualAccountMainBlockComponent,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
promoBannersBlockComponent: ComposableContentComponent? = null,
|
||||
promoBannersBlockComponent: PromoBannersBlockComponent? = null,
|
||||
walletId: String? = null,
|
||||
) {
|
||||
val containerColor = TangemTheme.colors2.surface.level1
|
||||
|
||||
|
|
@ -67,7 +69,11 @@ internal fun WalletListContent(
|
|||
|
||||
promoBannersBlockComponent?.let { component ->
|
||||
item(key = "PromoBannersBlock") {
|
||||
component.Content(modifier = Modifier.padding(top = TangemTheme.dimens2.x3))
|
||||
component.ContentWithPadding(
|
||||
horizontalItemPadding = 12.dp,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens2.x3),
|
||||
walletId = walletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.subscribers
|
||||
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.stories.GetStoryContentUseCase
|
||||
import com.tangem.domain.stories.models.StoryContentIds
|
||||
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetWalletNotificationsCarouselFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetWalletNotificationsFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class WalletNotificationsSubscriberTest {
|
||||
|
||||
private val stateHolder: WalletStateController = mockk(relaxed = true)
|
||||
private val clickIntents: WalletClickIntents = mockk(relaxed = true)
|
||||
private val getWalletNotificationsFactory: GetWalletNotificationsFactory = mockk()
|
||||
private val getWalletNotificationsCarouselFactory: GetWalletNotificationsCarouselFactory = mockk()
|
||||
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender = mockk(relaxed = true)
|
||||
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender = mockk(relaxed = true)
|
||||
private val getStoryContentUseCase: GetStoryContentUseCase = mockk()
|
||||
private val userWallet: UserWallet = mockk(relaxed = true)
|
||||
|
||||
private val subscriber = WalletNotificationsSubscriber(
|
||||
userWallet = userWallet,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
getWalletNotificationsFactory = getWalletNotificationsFactory,
|
||||
getWalletNotificationsCarouselFactory = getWalletNotificationsCarouselFactory,
|
||||
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
|
||||
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
|
||||
getStoryContentUseCase = getStoryContentUseCase,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearMocks(
|
||||
stateHolder,
|
||||
getWalletNotificationsFactory,
|
||||
getWalletNotificationsCarouselFactory,
|
||||
getStoryContentUseCase,
|
||||
userWallet,
|
||||
)
|
||||
every { userWallet.walletId } returns WALLET_ID
|
||||
|
||||
val walletUM = mockk<WalletUM>(relaxed = true)
|
||||
every { walletUM.walletsBalanceUM.id } returns WALLET_ID
|
||||
val screenState = mockk<WalletScreenState>(relaxed = true)
|
||||
every { screenState.wallets2 } returns persistentListOf(walletUM)
|
||||
every { stateHolder.uiState } returns MutableStateFlow(screenState)
|
||||
every { stateHolder.getWalletUM(any()) } returns walletUM
|
||||
|
||||
every { getWalletNotificationsFactory.create(any(), any()) } returns flowOf(persistentListOf())
|
||||
coEvery { getStoryContentUseCase.invokeSync(any(), any()) } returns null.right()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN carousel has yield boost banner WHEN subscribed THEN yield story is prefetched`() = runTest {
|
||||
// Arrange
|
||||
every { getWalletNotificationsCarouselFactory.create(any(), any()) } returns flowOf(yieldBoostCarousel())
|
||||
|
||||
// Act
|
||||
val dispatcher = UnconfinedTestDispatcher(testScheduler)
|
||||
val scope = CoroutineScope(dispatcher)
|
||||
subscriber.subscribe(scope, dispatcher)
|
||||
advanceUntilIdle()
|
||||
scope.cancel()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) {
|
||||
getStoryContentUseCase.invokeSync(id = StoryContentIds.STORY_FIRST_TIME_YIELD_PROMO.id, refresh = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN carousel has no yield boost banner WHEN subscribed THEN yield story is not prefetched`() = runTest {
|
||||
// Arrange
|
||||
every { getWalletNotificationsCarouselFactory.create(any(), any()) } returns flowOf(persistentListOf())
|
||||
|
||||
// Act
|
||||
val dispatcher = UnconfinedTestDispatcher(testScheduler)
|
||||
val scope = CoroutineScope(dispatcher)
|
||||
subscriber.subscribe(scope, dispatcher)
|
||||
advanceUntilIdle()
|
||||
scope.cancel()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 0) {
|
||||
getStoryContentUseCase.invokeSync(id = StoryContentIds.STORY_FIRST_TIME_YIELD_PROMO.id, refresh = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun yieldBoostCarousel(): ImmutableList<WalletNotificationUM> = persistentListOf(
|
||||
WalletNotificationUM.YieldBoostPromo(onExploreClick = {}, onLaterClick = {}),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val WALLET_ID = UserWalletId("01")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue