Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-26 16:03:11 +04:00
parent ff8f9c9b18
commit 6aab25427d
75 changed files with 2122 additions and 57 deletions

View file

@ -10,6 +10,11 @@ import com.tangem.domain.transaction.error.FeeErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
import com.tangem.domain.yield.supply.promo.YieldPromoRepository
import com.tangem.domain.yield.supply.promo.usecase.GetBoostedApyUseCase
import com.tangem.domain.yield.supply.promo.usecase.GetYieldBoostStatusUseCase
import com.tangem.domain.yield.supply.promo.usecase.IsYieldBoostPromoEnabledForTokenUseCase
import com.tangem.domain.yield.supply.promo.usecase.ShouldShowYieldBoostMainBannerUseCase
import com.tangem.domain.yield.supply.usecase.* import com.tangem.domain.yield.supply.usecase.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
@ -260,4 +265,32 @@ internal object YieldSupplyDomainModule {
coroutineScope = appScope, coroutineScope = appScope,
) )
} }
// region yield-boost promo ([REDACTED_TASK_KEY])
@Provides
@Singleton
fun provideGetBoostedApyUseCase(): GetBoostedApyUseCase = GetBoostedApyUseCase()
@Provides
@Singleton
fun provideGetYieldBoostStatusUseCase(repository: YieldPromoRepository): GetYieldBoostStatusUseCase {
return GetYieldBoostStatusUseCase(repository)
}
@Provides
@Singleton
fun provideIsYieldBoostPromoEnabledForTokenUseCase(
repository: YieldPromoRepository,
): IsYieldBoostPromoEnabledForTokenUseCase {
return IsYieldBoostPromoEnabledForTokenUseCase(repository)
}
@Provides
@Singleton
fun provideShouldShowYieldBoostMainBannerUseCase(
repository: YieldPromoRepository,
): ShouldShowYieldBoostMainBannerUseCase {
return ShouldShowYieldBoostMainBannerUseCase(repository)
}
// endregion
} }

View file

@ -290,6 +290,7 @@ internal class ChildFactory @Inject constructor(
storyId = route.storyId, storyId = route.storyId,
nextScreen = route.nextScreen, nextScreen = route.nextScreen,
screenSource = route.screenSource, screenSource = route.screenSource,
shouldMarkAsSeenOnClose = route.shouldMarkAsSeenOnClose,
), ),
componentFactory = storiesComponentFactory, componentFactory = storiesComponentFactory,
) )

View file

@ -350,6 +350,7 @@ sealed class AppRoute(val path: String) : Route {
val storyId: String, val storyId: String,
val nextScreen: AppRoute? = null, val nextScreen: AppRoute? = null,
val screenSource: String, val screenSource: String,
val shouldMarkAsSeenOnClose: Boolean = true,
) : AppRoute(path = "/stories$storyId") ) : AppRoute(path = "/stories$storyId")
@Serializable @Serializable

View file

@ -14,6 +14,8 @@ object TangemSiteUrlBuilder {
const val HELP_CENTER_SWAP_URL = const val HELP_CENTER_SWAP_URL =
"https://tangem.com/en/help-center/tangem-wallet-core-functionality/how-to-swap-coins-and-tokens/" "https://tangem.com/en/help-center/tangem-wallet-core-functionality/how-to-swap-coins-and-tokens/"
const val YIELD_MODE_TERMS_URL = "https://tangem.com/docs/en/yield-mode-terms.pdf"
suspend fun getUtmTags(campaign: String?): String { suspend fun getUtmTags(campaign: String?): String {
val langCode = Locale.getDefault().language val langCode = Locale.getDefault().language
val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty() val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty()

View file

@ -94,5 +94,9 @@
{ {
"name": "AND_15122_SWAP_PREDEFINED_BUTTONS_ENABLED", "name": "AND_15122_SWAP_PREDEFINED_BUTTONS_ENABLED",
"version": "undefined" "version": "undefined"
},
{
"name": "AND_15154_YIELD_PROMO_ENABLED",
"version": "undefined"
} }
] ]

View file

@ -0,0 +1,39 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class PromotionsResponse(
@Json(name = "promotions") val promotions: List<PromotionDto>,
) {
@JsonClass(generateAdapter = true)
data class PromotionDto(
@Json(name = "name") val name: String,
@Json(name = "all") val all: All?,
) {
@JsonClass(generateAdapter = true)
data class All(
@Json(name = "timeline") val timeline: Timeline,
@Json(name = "tokens") val tokens: List<PromoToken>?,
@Json(name = "status") val status: String,
@Json(name = "link") val link: String?,
)
@JsonClass(generateAdapter = true)
data class Timeline(
@Json(name = "start") val start: String,
@Json(name = "end") val end: String,
)
@JsonClass(generateAdapter = true)
data class PromoToken(
@Json(name = "tokenAddress") val tokenAddress: String,
@Json(name = "tokenSymbol") val tokenSymbol: String,
@Json(name = "tokenName") val tokenName: String,
@Json(name = "networkId") val networkId: String,
)
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class YieldBoostStatusResponse(
@Json(name = "tokenName") val tokenName: String?,
@Json(name = "networkId") val networkId: String?,
@Json(name = "moduleAddress") val moduleAddress: String?,
@Json(name = "userAddress") val userAddress: String?,
@Json(name = "contractAddress") val contractAddress: String?,
@Json(name = "promoEnrollmentStatus") val promoEnrollmentStatus: String,
@Json(name = "activationDate") val activationDate: String?,
@Json(name = "qualificationEndDate") val qualificationEndDate: String?,
@Json(name = "disqualificationReason") val disqualificationReason: String?,
)

View file

@ -1,6 +1,8 @@
package com.tangem.datasource.api.tangemTech package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.promotion.models.PromotionsResponse
import com.tangem.datasource.api.promotion.models.YieldBoostStatusResponse
import com.tangem.datasource.api.stories.models.StoryContentResponse import com.tangem.datasource.api.stories.models.StoryContentResponse
import com.tangem.datasource.api.tangemTech.models.* import com.tangem.datasource.api.tangemTech.models.*
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
@ -118,6 +120,18 @@ interface TangemTechApi {
@GET("v1/stories/{story_id}") @GET("v1/stories/{story_id}")
suspend fun getStoryById(@Path("story_id") storyId: String): ApiResponse<StoryContentResponse> suspend fun getStoryById(@Path("story_id") storyId: String): ApiResponse<StoryContentResponse>
// region yield-boost promo
@GET("/v2/promotion")
suspend fun getPromotions(
@Query("walletId") walletId: String,
@Header("Cache-Control") cacheControl: String = "max-age=600",
): ApiResponse<PromotionsResponse>
@Suppress("FunctionSignature", "TrailingCommaOnDeclarationSite")
@GET("/v2/promotion/yield-apr-boost/status")
suspend fun getYieldBoostStatus(@Query("walletId") walletId: String): ApiResponse<YieldBoostStatusResponse>
// endregion
// region push notifications // region push notifications
@GET("v1/notification/push_notifications_eligible_networks") @GET("v1/notification/push_notifications_eligible_networks")
suspend fun getEligibleNetworksForPushNotifications(): ApiResponse<List<CryptoNetworkResponse>> suspend fun getEligibleNetworksForPushNotifications(): ApiResponse<List<CryptoNetworkResponse>>

View file

@ -5,8 +5,13 @@ import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.datasource.local.yieldsupply.promo.DefaultYieldBoostPromoStore
import com.tangem.datasource.local.yieldsupply.promo.DefaultYieldBoostStatusStore
import com.tangem.datasource.local.yieldsupply.promo.YieldBoostPromoStore
import com.tangem.datasource.local.yieldsupply.promo.YieldBoostStatusStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes import com.tangem.datasource.utils.listTypes
import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.AppCoroutineScope
@ -40,4 +45,16 @@ object YieldSupplyModule {
), ),
) )
} }
@Provides
@Singleton
fun provideYieldBoostPromoStore(): YieldBoostPromoStore {
return DefaultYieldBoostPromoStore(dataStore = RuntimeSharedStore())
}
@Provides
@Singleton
fun provideYieldBoostStatusStore(): YieldBoostStatusStore {
return DefaultYieldBoostStatusStore(dataStore = RuntimeSharedStore())
}
} }

View file

@ -0,0 +1,20 @@
package com.tangem.datasource.local.yieldsupply.promo
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldBoostPromo
internal class DefaultYieldBoostPromoStore(
private val dataStore: RuntimeSharedStore<Map<UserWalletId, YieldBoostPromo>>,
) : YieldBoostPromoStore {
override suspend fun getSyncOrNull(userWalletId: UserWalletId): YieldBoostPromo? {
return dataStore.getSyncOrNull()?.get(userWalletId)
}
override suspend fun store(userWalletId: UserWalletId, value: YieldBoostPromo) {
dataStore.update(emptyMap()) { current ->
current + (userWalletId to value)
}
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.datasource.local.yieldsupply.promo
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldBoostStatus
internal class DefaultYieldBoostStatusStore(
private val dataStore: RuntimeSharedStore<Map<UserWalletId, YieldBoostStatus>>,
) : YieldBoostStatusStore {
override suspend fun getSyncOrNull(userWalletId: UserWalletId): YieldBoostStatus? {
return dataStore.getSyncOrNull()?.get(userWalletId)
}
override suspend fun store(userWalletId: UserWalletId, value: YieldBoostStatus) {
dataStore.update(emptyMap()) { current ->
current + (userWalletId to value)
}
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.yieldsupply.promo
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldBoostPromo
interface YieldBoostPromoStore {
suspend fun getSyncOrNull(userWalletId: UserWalletId): YieldBoostPromo?
suspend fun store(userWalletId: UserWalletId, value: YieldBoostPromo)
}

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.yieldsupply.promo
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldBoostStatus
interface YieldBoostStatusStore {
suspend fun getSyncOrNull(userWalletId: UserWalletId): YieldBoostStatus?
suspend fun store(userWalletId: UserWalletId, value: YieldBoostStatus)
}

View file

@ -403,6 +403,7 @@
<string name="common_sending">Senden</string> <string name="common_sending">Senden</string>
<string name="common_sent">Gesendet</string> <string name="common_sent">Gesendet</string>
<string name="common_server_unavailable">Der Server ist nicht verfügbar. Bitte versuche es später erneut.</string> <string name="common_server_unavailable">Der Server ist nicht verfügbar. Bitte versuche es später erneut.</string>
<string name="common_session_expired">Sitzung abgelaufen</string>
<string name="common_share">Teilen</string> <string name="common_share">Teilen</string>
<string name="common_share_link">Link teilen</string> <string name="common_share_link">Link teilen</string>
<string name="common_show_less">Weniger anzeigen</string> <string name="common_show_less">Weniger anzeigen</string>
@ -1209,7 +1210,9 @@
<string name="organize_tokens_title">Token organisieren</string> <string name="organize_tokens_title">Token organisieren</string>
<string name="organize_tokens_ungroup">Gruppe löschen</string> <string name="organize_tokens_ungroup">Gruppe löschen</string>
<string name="provider_name_support">%s Unterstützung</string> <string name="provider_name_support">%s Unterstützung</string>
<string name="push_notification_settings_banner_button_grant_permission">Genehmigung erteilen</string>
<string name="push_notification_settings_banner_description">Push-Benachrichtigungen sind aktiviert, funktionieren aber erst, nachdem du Benachrichtigungen in den Geräteeinstellungen zugelassen hast.</string> <string name="push_notification_settings_banner_description">Push-Benachrichtigungen sind aktiviert, funktionieren aber erst, nachdem du Benachrichtigungen in den Geräteeinstellungen zugelassen hast.</string>
<string name="push_notification_settings_banner_description_grant_permission">Push-Benachrichtigungen sind aktiviert, funktionieren aber erst nach Ihrer Zustimmung.</string>
<string name="push_notification_settings_banner_title">Benachrichtigungen zulassen</string> <string name="push_notification_settings_banner_title">Benachrichtigungen zulassen</string>
<string name="push_notification_settings_offers_updates_subtitle">Produktneuheiten, exklusive Angebote und Erinnerungen an Aktivitäten.</string> <string name="push_notification_settings_offers_updates_subtitle">Produktneuheiten, exklusive Angebote und Erinnerungen an Aktivitäten.</string>
<string name="push_notification_settings_offers_updates_title">Angebote &amp; Updates</string> <string name="push_notification_settings_offers_updates_title">Angebote &amp; Updates</string>
@ -1853,6 +1856,10 @@
<string name="tangempay_tangem_visa_card">Nutzen Sie USDC für alltägliche Zahlungen</string> <string name="tangempay_tangem_visa_card">Nutzen Sie USDC für alltägliche Zahlungen</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay ist vorübergehend nicht erreichbar.</string> <string name="tangempay_temporarily_unavailable">Tangem Pay ist vorübergehend nicht erreichbar.</string>
<string name="tangempay_title">Tangem Pay</string> <string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">Senden Sie USDC Polygon an die Adresse Ihres Kontos</string>
<string name="tangempay_topup_receive_title">Von einer anderen Wallet oder Börse</string>
<string name="tangempay_topup_swap_body">Tauschen Sie beliebige Assets in USDC Polygon um</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_usdc_on_polygon_network">USDC im Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Klicken Sie auf die Schaltfläche unten, um den Zugriff wiederherzustellen</string> <string name="tangempay_use_tangem_device_to_restore_payment_account">Klicken Sie auf die Schaltfläche unten, um den Zugriff wiederherzustellen</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_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>
@ -2369,6 +2376,10 @@
<string name="yield_apy_boost_banner_title">Sonderangebot für den Yield-Modus</string> <string name="yield_apy_boost_banner_title">Sonderangebot für den Yield-Modus</string>
<string name="yield_apy_boost_banner_title_apy_multiplied">APY x3</string> <string name="yield_apy_boost_banner_title_apy_multiplied">APY x3</string>
<string name="yield_apy_boost_block_activate">yield_apy_boost_block_activate</string> <string name="yield_apy_boost_block_activate">yield_apy_boost_block_activate</string>
<string name="yield_apy_boost_promo_activate_bonus">Aktivieren dein Bonus</string>
<string name="yield_apy_boost_promo_bonus_paid_out_subtitle">Transaktionsverlauf für Details prüfen</string>
<string name="yield_apy_boost_promo_bonus_paid_out_title">Bonus im Ertragsmodus ausgezahlt</string>
<string name="yield_apy_boost_promo_days_left_to_unlock">%1$s tage übrig, um dein Bonus freizuschalten</string>
<string name="yield_apy_boost_promo_eligibility_text">Du hast Anspruch auf einen 30-tägigen APY-Boost, es gelten die T&amp;C, erfahren Sie mehr</string> <string name="yield_apy_boost_promo_eligibility_text">Du hast Anspruch auf einen 30-tägigen APY-Boost, es gelten die T&amp;C, erfahren Sie mehr</string>
<string name="yield_apy_boost_story_first_subtitle">Aktiviere den Renditemodus zum ersten Mal und erhalte in den ersten 30 Tagen bis zu 3x Rendite</string> <string name="yield_apy_boost_story_first_subtitle">Aktiviere den Renditemodus zum ersten Mal und erhalte in den ersten 30 Tagen bis zu 3x Rendite</string>
<string name="yield_apy_boost_story_first_title">Bonus für den ersten Monat APR</string> <string name="yield_apy_boost_story_first_title">Bonus für den ersten Monat APR</string>
@ -2479,5 +2490,6 @@
<string name="yield_module_unavailable_subtitle">Der Yield-Modus ist momentan nicht verfügbar. Bitte versuche es später erneut.</string> <string name="yield_module_unavailable_subtitle">Der Yield-Modus ist momentan nicht verfügbar. Bitte versuche es später erneut.</string>
<string name="yield_module_unavailable_title">Yield Mode nicht verfügbar</string> <string name="yield_module_unavailable_title">Yield Mode nicht verfügbar</string>
<string name="yield_promo_completed">Die Berechtigung zur Bonusauszahlung wird geprüft</string> <string name="yield_promo_completed">Die Berechtigung zur Bonusauszahlung wird geprüft</string>
<string name="yield_promo_left_title">Um Ihren Bonus freizuschalten, müssen Sie nur noch wenige Schritte verbleiben.</string>
<string name="yield_supply_chart_loading_error">Chart konnte nicht geladen werden...</string> <string name="yield_supply_chart_loading_error">Chart konnte nicht geladen werden...</string>
</resources> </resources>

View file

@ -80,6 +80,7 @@
<string name="action_buttons_swap_not_enough_tokens_alert_title">Añada tokens</string> <string name="action_buttons_swap_not_enough_tokens_alert_title">Añada tokens</string>
<string name="action_buttons_you_want_to_receive">Seleccione el token que desea recibir</string> <string name="action_buttons_you_want_to_receive">Seleccione el token que desea recibir</string>
<string name="action_buttons_you_want_to_swap">Seleccione el token que desea intercambiar</string> <string name="action_buttons_you_want_to_swap">Seleccione el token que desea intercambiar</string>
<string name="add_and_manage_sheet_manage_title">Agregar tokens</string>
<string name="add_custom_token_choose_network">Elige red</string> <string name="add_custom_token_choose_network">Elige red</string>
<string name="add_custom_token_title">Agregue un token personalizado</string> <string name="add_custom_token_title">Agregue un token personalizado</string>
<string name="add_tokens_title">Gestionar tokens</string> <string name="add_tokens_title">Gestionar tokens</string>
@ -569,6 +570,7 @@
<string name="express_provider">Proveedor</string> <string name="express_provider">Proveedor</string>
<string name="express_provider_best_rate">Mejor tarifa</string> <string name="express_provider_best_rate">Mejor tarifa</string>
<string name="express_provider_fca_warning_list">Lista de advertencias de la FCA</string> <string name="express_provider_fca_warning_list">Lista de advertencias de la FCA</string>
<string name="express_provider_for_swap">Proveedor de intercambio</string>
<string name="express_provider_great_rate">Mejor opción</string> <string name="express_provider_great_rate">Mejor opción</string>
<string name="express_provider_in_fca_warning_list">Proveedor en la lista de advertencias de la FCA</string> <string name="express_provider_in_fca_warning_list">Proveedor en la lista de advertencias de la FCA</string>
<string name="express_provider_max_amount">Disponible hasta %s</string> <string name="express_provider_max_amount">Disponible hasta %s</string>
@ -731,6 +733,7 @@
<string name="koinos_mana_exceeds_koin_balance_title">Límite de Mana</string> <string name="koinos_mana_exceeds_koin_balance_title">Límite de Mana</string>
<string name="koinos_mana_level_description">La red Koinos requiere Mana para las tarifas de red. Tienes %1$s/%2$s Mana</string> <string name="koinos_mana_level_description">La red Koinos requiere Mana para las tarifas de red. Tienes %1$s/%2$s Mana</string>
<string name="koinos_mana_level_title">Nivel de Mana</string> <string name="koinos_mana_level_title">Nivel de Mana</string>
<string name="main_add_and_manage_tokens">Añadir y Gestionar</string>
<string name="main_empty_tokens_list_message">Para hacer un seguimiento de sus criptomonedas y transacciones, agregue tokens</string> <string name="main_empty_tokens_list_message">Para hacer un seguimiento de sus criptomonedas y transacciones, agregue tokens</string>
<string name="main_manage_tokens">Gestionar tokens</string> <string name="main_manage_tokens">Gestionar tokens</string>
<string name="main_qr_scan_hint">Escanee el código QR para enviar fondos o conectarse a una aplicación</string> <string name="main_qr_scan_hint">Escanee el código QR para enviar fondos o conectarse a una aplicación</string>
@ -1089,16 +1092,29 @@
<string name="onramp_error_transaction_already_processed">Esta transacción ya ha sido procesada. No se requiere ninguna otra acción.</string> <string name="onramp_error_transaction_already_processed">Esta transacción ya ha sido procesada. No se requiere ninguna otra acción.</string>
<string name="onramp_fetching_best_rates">Obteniendo las mejores tarifas...</string> <string name="onramp_fetching_best_rates">Obteniendo las mejores tarifas...</string>
<string name="onramp_instant_status">Instantáneo</string> <string name="onramp_instant_status">Instantáneo</string>
<string name="onramp_kyc_verification_bullet_free">La verificación es gratuita y suele tardar entre 1 y 2 minutos.</string>
<string name="onramp_kyc_verification_bullet_privacy">Tangem no tendrá acceso a su información de identidad, usted comparte los datos directamente con el proveedor regulado</string>
<string name="onramp_kyc_verification_bullet_unlocks">La verificación desbloquea el acceso completo a futuras transacciones con este proveedor</string>
<string name="onramp_kyc_verification_choose_another">Elija otro método</string>
<string name="onramp_kyc_verification_subtitle">Para cumplir los requisitos normativos locales, %@ exige la verificación de su identidad.</string>
<string name="onramp_kyc_verification_title">Verificación de identidad requerida por el proveedor de pago</string>
<string name="onramp_kyc_verification_verify_button">Verificar</string>
<string name="onramp_kyc_verification_whats_important">Lo importante</string>
<string name="onramp_legal">Al utilizar la funcionalidad onramp, acepta %1$s y %2$s del proveedor.</string> <string name="onramp_legal">Al utilizar la funcionalidad onramp, acepta %1$s y %2$s del proveedor.</string>
<string name="onramp_legal_text">El servicio es proporcionado por un proveedor externo. \nTangem no es responsable.</string> <string name="onramp_legal_text">El servicio es proporcionado por un proveedor externo. \nTangem no es responsable.</string>
<string name="onramp_max_amount_restriction">El monto de la compra no debe ser mayor a %s</string> <string name="onramp_max_amount_restriction">El monto de la compra no debe ser mayor a %s</string>
<string name="onramp_min_amount_restriction">La cantidad a comprar debe ser como mínimo %s</string> <string name="onramp_min_amount_restriction">La cantidad a comprar debe ser como mínimo %s</string>
<string name="onramp_native_payment_cumulative_limit" formatted="false">El importe acumulado de la transacción superior a %1s puede requerir la verificación de la identidad con %2s</string>
<string name="onramp_native_payment_cumulative_limit_equivalent" formatted="false">El importe acumulado de la transacción superior al equivalente de %1s puede requerir la verificación de la identidad con %2s</string>
<string name="onramp_native_payment_legal_notice" formatted="false">Al hacer clic en Pagar, usted acepta %1s\'s %2s y %3s.</string>
<string name="onramp_no_available_providers">No hay proveedores disponibles para esta moneda</string> <string name="onramp_no_available_providers">No hay proveedores disponibles para esta moneda</string>
<string name="onramp_offer_type_fastet">Procesamiento más rápido</string> <string name="onramp_offer_type_fastet">Procesamiento más rápido</string>
<string name="onramp_pay_with">Pagar con</string> <string name="onramp_pay_with">Pagar con</string>
<string name="onramp_payment_method_subtitle">Método de pago</string> <string name="onramp_payment_method_subtitle">Método de pago</string>
<string name="onramp_provider_max_amount">Disponible hasta %s</string> <string name="onramp_provider_max_amount">Disponible hasta %s</string>
<string name="onramp_provider_min_amount">Disponible desde %s</string> <string name="onramp_provider_min_amount">Disponible desde %s</string>
<string name="onramp_provider_requirements_body">Las tarjetas emitidas en EE.UU. y el Reino Unido no pueden procesarse por este método. El proveedor puede requerir una verificación de identidad adicional</string>
<string name="onramp_provider_requirements_title">Requisitos del proveedor</string>
<plurals name="onramp_providers_count"> <plurals name="onramp_providers_count">
<item quantity="one">%d proveedor</item> <item quantity="one">%d proveedor</item>
<item quantity="other">%d proveedores</item> <item quantity="other">%d proveedores</item>
@ -1508,6 +1524,7 @@
<string name="sui_not_enough_coin_for_fee_description">Se requiere una transacción entrante de al menos %1$s para proceder</string> <string name="sui_not_enough_coin_for_fee_description">Se requiere una transacción entrante de al menos %1$s para proceder</string>
<string name="sui_not_enough_coin_for_fee_title">Fondos insuficientes</string> <string name="sui_not_enough_coin_for_fee_title">Fondos insuficientes</string>
<string name="swap_approve_description">Al aprobar, permites que el contrato inteligente use tus tokens en futuras transacciones.</string> <string name="swap_approve_description">Al aprobar, permites que el contrato inteligente use tus tokens en futuras transacciones.</string>
<string name="swap_detailed_mode">Modo detallado</string>
<string name="swap_fixed_rate">Tasa Fija</string> <string name="swap_fixed_rate">Tasa Fija</string>
<string name="swap_give_permission_fee_footer">La red cobrará una tasa de aprobación del token para verificar que usted autoriza el uso de su token para el intercambio.</string> <string name="swap_give_permission_fee_footer">La red cobrará una tasa de aprobación del token para verificar que usted autoriza el uso de su token para el intercambio.</string>
<string name="swap_in_progress">Intercambio en curso</string> <string name="swap_in_progress">Intercambio en curso</string>
@ -1516,6 +1533,7 @@
<string name="swap_search_suggestion_hint">¿Busca algo más?\n¡Intente buscar o explorar otra criptomoneda!</string> <string name="swap_search_suggestion_hint">¿Busca algo más?\n¡Intente buscar o explorar otra criptomoneda!</string>
<string name="swap_search_tooltip_description">Busque cualquier token, incluso si aún no está en su lista.</string> <string name="swap_search_tooltip_description">Busque cualquier token, incluso si aún no está en su lista.</string>
<string name="swap_search_tooltip_title">Utilice la búsqueda para encontrar lo que necesite</string> <string name="swap_search_tooltip_title">Utilice la búsqueda para encontrar lo que necesite</string>
<string name="swap_simple_mode">Modo sencillo</string>
<string name="swap_story_fifth_subtitle">Siéntase seguro con una asistencia permanente que le ayudará con cualquier problema</string> <string name="swap_story_fifth_subtitle">Siéntase seguro con una asistencia permanente que le ayudará con cualquier problema</string>
<string name="swap_story_fifth_title">Siempre aquí</string> <string name="swap_story_fifth_title">Siempre aquí</string>
<string name="swap_story_first_subtitle">Múltiples proveedores de confianza en un solo lugar: intercambie cualquier activo sin esfuerzo en su billetera</string> <string name="swap_story_first_subtitle">Múltiples proveedores de confianza en un solo lugar: intercambie cualquier activo sin esfuerzo en su billetera</string>
@ -1552,6 +1570,10 @@
<string name="swapping_insufficient_funds">Fondos insuficientes</string> <string name="swapping_insufficient_funds">Fondos insuficientes</string>
<string name="swapping_insufficient_funds_description">No hay fondos suficientes para completar esta transacción. Reduzca el importe a recibir o añada más fondos.</string> <string name="swapping_insufficient_funds_description">No hay fondos suficientes para completar esta transacción. Reduzca el importe a recibir o añada más fondos.</string>
<string name="swapping_permission_header">Dar autorización</string> <string name="swapping_permission_header">Dar autorización</string>
<string name="swapping_rate_experience_title">Valore su experiencia con el proveedor</string>
<string name="swapping_rate_feedback_placeholder">Escriba sus comentarios</string>
<string name="swapping_rate_feedback_submit">Enviar comentarios</string>
<string name="swapping_rate_feedback_title">¿Qué influyó en su \nexperiencia?</string>
<string name="swapping_swap_action">Intercambiar</string> <string name="swapping_swap_action">Intercambiar</string>
<string name="swapping_swap_action_in_progress">Intercambiando...</string> <string name="swapping_swap_action_in_progress">Intercambiando...</string>
<string name="swapping_to_account_title">Usted recibe</string> <string name="swapping_to_account_title">Usted recibe</string>
@ -1724,6 +1746,10 @@
<string name="tangempay_tangem_visa_card">Usa USDC para pagos cotidianos</string> <string name="tangempay_tangem_visa_card">Usa USDC para pagos cotidianos</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay no está disponible temporalmente.</string> <string name="tangempay_temporarily_unavailable">Tangem Pay no está disponible temporalmente.</string>
<string name="tangempay_title">Tangem Pay</string> <string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">Envía USDC Polygon a la dirección de tu cuenta</string>
<string name="tangempay_topup_receive_title">Desde otra billetera o exchange</string>
<string name="tangempay_topup_swap_body">Intercambia cualquier activo por USDC Polygon</string>
<string name="tangempay_topup_swap_title">Desde tu Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC en Polygon</string> <string name="tangempay_usdc_on_polygon_network">USDC en Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Haga clic en el botón de abajo para restaurar el acceso</string> <string name="tangempay_use_tangem_device_to_restore_payment_account">Haga clic en el botón de abajo para restaurar el acceso</string>
<string name="tangempay_withdrawal_note_description">Los fondos de compras reembolsadas no se devolverán a tu saldo on-chain Polygon ni estarán disponibles para retiro, pero permanecerán en tu saldo de tarjeta para compras</string> <string name="tangempay_withdrawal_note_description">Los fondos de compras reembolsadas no se devolverán a tu saldo on-chain Polygon ni estarán disponibles para retiro, pero permanecerán en tu saldo de tarjeta para compras</string>

View file

@ -1497,6 +1497,10 @@
<string name="swapping_high_price_impact_title">Impact élevé sur les prix</string> <string name="swapping_high_price_impact_title">Impact élevé sur les prix</string>
<string name="swapping_insufficient_funds">Fonds insuffisants</string> <string name="swapping_insufficient_funds">Fonds insuffisants</string>
<string name="swapping_permission_header">Donner l\'autorisation</string> <string name="swapping_permission_header">Donner l\'autorisation</string>
<string name="swapping_rate_experience_title">Évaluez votre expérience avec ce prestataire</string>
<string name="swapping_rate_feedback_placeholder">Saisissez votre avis</string>
<string name="swapping_rate_feedback_submit">Envoyez votre avis</string>
<string name="swapping_rate_feedback_title">Qu\'est-ce qui a influencé votre \nexpérience ?</string>
<string name="swapping_swap_action">Échanger</string> <string name="swapping_swap_action">Échanger</string>
<string name="swapping_swap_action_in_progress">Échange...</string> <string name="swapping_swap_action_in_progress">Échange...</string>
<string name="swapping_to_account_title">Vous recevez à</string> <string name="swapping_to_account_title">Vous recevez à</string>
@ -1666,6 +1670,10 @@
<string name="tangempay_tangem_visa_card">Utilisez USDC pour les paiements quotidiens</string> <string name="tangempay_tangem_visa_card">Utilisez USDC pour les paiements quotidiens</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay est temporairement indisponible</string> <string name="tangempay_temporarily_unavailable">Tangem Pay est temporairement indisponible</string>
<string name="tangempay_title">Tangem Pay</string> <string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">Envoyez USDC Polygon à l\'adresse de votre compte</string>
<string name="tangempay_topup_receive_title">Depuis un autre wallet ou exchange</string>
<string name="tangempay_topup_swap_body">Échangez n\'importe quel actif contre USDC Polygon</string>
<string name="tangempay_topup_swap_title">Depuis votre Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC sur Polygon</string> <string name="tangempay_usdc_on_polygon_network">USDC sur Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Cliquez sur le bouton ci-dessous pour restaurer l\'accès</string> <string name="tangempay_use_tangem_device_to_restore_payment_account">Cliquez sur le bouton ci-dessous pour restaurer l\'accès</string>
<string name="tangempay_withdrawal_note_description">Les fonds des achats remboursés ne seront pas retournés à votre solde sur Polygon ni disponibles pour un retrait, mais resteront sur votre solde de carte pour vos achats</string> <string name="tangempay_withdrawal_note_description">Les fonds des achats remboursés ne seront pas retournés à votre solde sur Polygon ni disponibles pour un retrait, mais resteront sur votre solde de carte pour vos achats</string>

View file

@ -218,6 +218,10 @@
<string name="tangempay_tangem_visa_card">Usa USDC per i pagamenti quotidiani</string> <string name="tangempay_tangem_visa_card">Usa USDC per i pagamenti quotidiani</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay è temporaneamente non disponibile</string> <string name="tangempay_temporarily_unavailable">Tangem Pay è temporaneamente non disponibile</string>
<string name="tangempay_title">Tangem Pay</string> <string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">Invia USDC Polygon all\'indirizzo del tuo account</string>
<string name="tangempay_topup_receive_title">Da un altro wallet o exchange</string>
<string name="tangempay_topup_swap_body">Converti qualsiasi asset in USDC Polygon</string>
<string name="tangempay_topup_swap_title">Dal tuo Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC sulla Polygon</string> <string name="tangempay_usdc_on_polygon_network">USDC sulla Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Fare clic sul pulsante in basso per ripristinare l\'accesso</string> <string name="tangempay_use_tangem_device_to_restore_payment_account">Fare clic sul pulsante in basso per ripristinare l\'accesso</string>
<string name="tangempay_withdrawal_note_description">I fondi degli acquisti rimborsati non verranno restituiti al tuo saldo on-chain Polygon né saranno disponibili per il prelievo, ma rimarranno sul saldo della tua carta per gli acquisti</string> <string name="tangempay_withdrawal_note_description">I fondi degli acquisti rimborsati non verranno restituiti al tuo saldo on-chain Polygon né saranno disponibili per il prelievo, ma rimarranno sul saldo della tua carta per gli acquisti</string>

View file

@ -1187,7 +1187,9 @@
<string name="organize_tokens_title">トークンを整理する</string> <string name="organize_tokens_title">トークンを整理する</string>
<string name="organize_tokens_ungroup">グループ解除</string> <string name="organize_tokens_ungroup">グループ解除</string>
<string name="provider_name_support">%sサポート</string> <string name="provider_name_support">%sサポート</string>
<string name="push_notification_settings_banner_button_grant_permission">許可する</string>
<string name="push_notification_settings_banner_description">プッシュ通知は有効になっていますが、端末の設定で通知を許可するまで動作しません。</string> <string name="push_notification_settings_banner_description">プッシュ通知は有効になっていますが、端末の設定で通知を許可するまで動作しません。</string>
<string name="push_notification_settings_banner_description_grant_permission">プッシュ通知は有効になっていますが、許可するまで機能しません</string>
<string name="push_notification_settings_banner_title">通知を許可する</string> <string name="push_notification_settings_banner_title">通知を許可する</string>
<string name="push_notification_settings_offers_updates_subtitle">製品ニュース、限定オファー、アクティビティのリマインダー。</string> <string name="push_notification_settings_offers_updates_subtitle">製品ニュース、限定オファー、アクティビティのリマインダー。</string>
<string name="push_notification_settings_offers_updates_title">オファー・最新情報</string> <string name="push_notification_settings_offers_updates_title">オファー・最新情報</string>
@ -1827,6 +1829,10 @@
<string name="tangempay_tangem_visa_card">日常の支払いにUSDCを利用</string> <string name="tangempay_tangem_visa_card">日常の支払いにUSDCを利用</string>
<string name="tangempay_temporarily_unavailable">Tangem Payは現在一時的に利用できません。</string> <string name="tangempay_temporarily_unavailable">Tangem Payは現在一時的に利用できません。</string>
<string name="tangempay_title">Tangem Pay</string> <string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">USDC Polygon をアカウントのアドレスに送信</string>
<string name="tangempay_topup_receive_title">別のウォレットまたは取引所から</string>
<string name="tangempay_topup_swap_body">任意の資産を USDC Polygon にスワップ</string>
<string name="tangempay_topup_swap_title">Tangem ウォレットから</string>
<string name="tangempay_usdc_on_polygon_network">Polygonネットワーク上のUSDC</string> <string name="tangempay_usdc_on_polygon_network">Polygonネットワーク上のUSDC</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">下のボタンをクリックしてアクセスを復元してください</string> <string name="tangempay_use_tangem_device_to_restore_payment_account">下のボタンをクリックしてアクセスを復元してください</string>
<string name="tangempay_withdrawal_note_description">返金分はオンチェーンのPolygon残高には戻らず、出金にも利用できません。ただし、カード残高として残り、支払いに利用できます。</string> <string name="tangempay_withdrawal_note_description">返金分はオンチェーンのPolygon残高には戻らず、出金にも利用できません。ただし、カード残高として残り、支払いに利用できます。</string>

View file

@ -403,6 +403,7 @@
<string name="common_sending">Enviando</string> <string name="common_sending">Enviando</string>
<string name="common_sent">Enviado</string> <string name="common_sent">Enviado</string>
<string name="common_server_unavailable">O servidor não está disponível. Tente novamente mais tarde.</string> <string name="common_server_unavailable">O servidor não está disponível. Tente novamente mais tarde.</string>
<string name="common_session_expired">Sessão expirada</string>
<string name="common_share">Compartilhar</string> <string name="common_share">Compartilhar</string>
<string name="common_share_link">Compartilhar link</string> <string name="common_share_link">Compartilhar link</string>
<string name="common_show_less">Mostrar menos</string> <string name="common_show_less">Mostrar menos</string>
@ -1209,7 +1210,9 @@
<string name="organize_tokens_title">Organizar tokens</string> <string name="organize_tokens_title">Organizar tokens</string>
<string name="organize_tokens_ungroup">Desagrupar</string> <string name="organize_tokens_ungroup">Desagrupar</string>
<string name="provider_name_support">%s suporte</string> <string name="provider_name_support">%s suporte</string>
<string name="push_notification_settings_banner_button_grant_permission">Conceder permissão</string>
<string name="push_notification_settings_banner_description">As notificações push estão ativadas, mas só funcionarão depois que você as permitir nas configurações do seu dispositivo.</string> <string name="push_notification_settings_banner_description">As notificações push estão ativadas, mas só funcionarão depois que você as permitir nas configurações do seu dispositivo.</string>
<string name="push_notification_settings_banner_description_grant_permission">As notificações push estão ativadas, mas não funcionarão até que você conceda permissão.</string>
<string name="push_notification_settings_banner_title">Permitir notificações</string> <string name="push_notification_settings_banner_title">Permitir notificações</string>
<string name="push_notification_settings_offers_updates_subtitle">Novidades sobre produtos, ofertas exclusivas e lembretes de atividades.</string> <string name="push_notification_settings_offers_updates_subtitle">Novidades sobre produtos, ofertas exclusivas e lembretes de atividades.</string>
<string name="push_notification_settings_offers_updates_title">Ofertas e atualizações</string> <string name="push_notification_settings_offers_updates_title">Ofertas e atualizações</string>
@ -1853,6 +1856,10 @@
<string name="tangempay_tangem_visa_card">Use USDC para pagamentos do dia a dia.</string> <string name="tangempay_tangem_visa_card">Use USDC para pagamentos do dia a dia.</string>
<string name="tangempay_temporarily_unavailable">O serviço Tangem Pay está temporariamente inacessível.</string> <string name="tangempay_temporarily_unavailable">O serviço Tangem Pay está temporariamente inacessível.</string>
<string name="tangempay_title">Tangem Pay</string> <string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">Envie USDC Polygon para o endereço da sua conta</string>
<string name="tangempay_topup_receive_title">De outra carteira ou exchange</string>
<string name="tangempay_topup_swap_body">Troque qualquer ativo por USDC Polygon</string>
<string name="tangempay_topup_swap_title">Da sua Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC na rede Polygon</string> <string name="tangempay_usdc_on_polygon_network">USDC na rede Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Clique no botão abaixo para restaurar o acesso.</string> <string name="tangempay_use_tangem_device_to_restore_payment_account">Clique no botão abaixo para restaurar o acesso.</string>
<string name="tangempay_withdrawal_note_description">Os fundos de compras reembolsadas não serão devolvidos ao seu saldo na blockchain nem estarão disponíveis para saque, mas permanecerão no saldo do seu cartão para compras futuras</string> <string name="tangempay_withdrawal_note_description">Os fundos de compras reembolsadas não serão devolvidos ao seu saldo na blockchain nem estarão disponíveis para saque, mas permanecerão no saldo do seu cartão para compras futuras</string>
@ -2369,6 +2376,10 @@
<string name="yield_apy_boost_banner_title">Oferta especial para o modo Yield</string> <string name="yield_apy_boost_banner_title">Oferta especial para o modo Yield</string>
<string name="yield_apy_boost_banner_title_apy_multiplied">APY x3</string> <string name="yield_apy_boost_banner_title_apy_multiplied">APY x3</string>
<string name="yield_apy_boost_block_activate">yield_apy_boost_block_activate</string> <string name="yield_apy_boost_block_activate">yield_apy_boost_block_activate</string>
<string name="yield_apy_boost_promo_activate_bonus">Ative seu bônus</string>
<string name="yield_apy_boost_promo_bonus_paid_out_subtitle">Consulte o histórico de transações para obter detalhes.</string>
<string name="yield_apy_boost_promo_bonus_paid_out_title">Bônus do modo Yield pago</string>
<string name="yield_apy_boost_promo_days_left_to_unlock">%1$s Faltam poucos dias para desbloquear seu bônus.</string>
<string name="yield_apy_boost_promo_eligibility_text">Você tem direito a um aumento de APY por 30 dias. Aplicam-se os termos e condições. Saiba mais.</string> <string name="yield_apy_boost_promo_eligibility_text">Você tem direito a um aumento de APY por 30 dias. Aplicam-se os termos e condições. Saiba mais.</string>
<string name="yield_apy_boost_story_first_subtitle">Ative o Modo de Rendimento pela primeira vez e obtenha até 3 vezes mais rendimento nos seus primeiros 30 dias.</string> <string name="yield_apy_boost_story_first_subtitle">Ative o Modo de Rendimento pela primeira vez e obtenha até 3 vezes mais rendimento nos seus primeiros 30 dias.</string>
<string name="yield_apy_boost_story_first_title">Bônus de APR no primeiro mês</string> <string name="yield_apy_boost_story_first_title">Bônus de APR no primeiro mês</string>

View file

@ -84,7 +84,7 @@
<string name="actionbutton_swap_title">Обмен</string> <string name="actionbutton_swap_title">Обмен</string>
<string name="actionbutton_transfer_title">Перевод</string> <string name="actionbutton_transfer_title">Перевод</string>
<string name="add_and_manage_sheet_manage_subtitle">Добавить в портфель</string> <string name="add_and_manage_sheet_manage_subtitle">Добавить в портфель</string>
<string name="add_and_manage_sheet_manage_title">Добавить токен</string> <string name="add_and_manage_sheet_manage_title">Добавить токены</string>
<string name="add_and_manage_sheet_organize_subtitle">Сортировка и группировка</string> <string name="add_and_manage_sheet_organize_subtitle">Сортировка и группировка</string>
<string name="add_and_manage_sheet_organize_title">Упорядочить токены</string> <string name="add_and_manage_sheet_organize_title">Упорядочить токены</string>
<string name="add_custom_token_choose_network">Выберите сеть</string> <string name="add_custom_token_choose_network">Выберите сеть</string>
@ -617,6 +617,7 @@
<string name="express_provider">Провайдер</string> <string name="express_provider">Провайдер</string>
<string name="express_provider_best_rate">Лучший курс</string> <string name="express_provider_best_rate">Лучший курс</string>
<string name="express_provider_fixed_rate_is_unavailable">Фиксированная ставка недоступна</string> <string name="express_provider_fixed_rate_is_unavailable">Фиксированная ставка недоступна</string>
<string name="express_provider_for_swap">Провайдер для обмена</string>
<string name="express_provider_great_rate">Лучший выбор</string> <string name="express_provider_great_rate">Лучший выбор</string>
<string name="express_provider_max_amount">Доступно до %s</string> <string name="express_provider_max_amount">Доступно до %s</string>
<string name="express_provider_min_amount">Доступно с %s</string> <string name="express_provider_min_amount">Доступно с %s</string>
@ -780,7 +781,7 @@
<string name="koinos_mana_exceeds_koin_balance_title">Лимит маны</string> <string name="koinos_mana_exceeds_koin_balance_title">Лимит маны</string>
<string name="koinos_mana_level_description">Сеть Koinos использует Ману для оплаты комиссии сети. У вас есть %1$s/%2$s Mana</string> <string name="koinos_mana_level_description">Сеть Koinos использует Ману для оплаты комиссии сети. У вас есть %1$s/%2$s Mana</string>
<string name="koinos_mana_level_title">Уровень маны</string> <string name="koinos_mana_level_title">Уровень маны</string>
<string name="main_add_and_manage_tokens">Добавить и настроить</string> <string name="main_add_and_manage_tokens">Добавить и управлять</string>
<string name="main_empty_tokens_list_message">Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены</string> <string name="main_empty_tokens_list_message">Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены</string>
<string name="main_manage_tokens">Управление токенами</string> <string name="main_manage_tokens">Управление токенами</string>
<string name="main_qr_scan_hint">Отсканируйте QR-код, чтобы отправить средства или подключиться к приложению.</string> <string name="main_qr_scan_hint">Отсканируйте QR-код, чтобы отправить средства или подключиться к приложению.</string>
@ -1160,15 +1161,26 @@
<string name="onramp_error_transaction_already_processed">Эта транзакция уже была обработана. Дополнительных действий не требуется.</string> <string name="onramp_error_transaction_already_processed">Эта транзакция уже была обработана. Дополнительных действий не требуется.</string>
<string name="onramp_fetching_best_rates">Получение лучших курсов...</string> <string name="onramp_fetching_best_rates">Получение лучших курсов...</string>
<string name="onramp_instant_status">Моментально</string> <string name="onramp_instant_status">Моментально</string>
<string name="onramp_kyc_verification_bullet_free">Верификация бесплатная и обычно занимает 1-2 минуты</string>
<string name="onramp_kyc_verification_bullet_privacy">Tangem не будет иметь доступа к вашим личным данным, вы передаете их напрямую лицензированному провайдеру</string>
<string name="onramp_kyc_verification_choose_another">Выберите другой метод</string>
<string name="onramp_kyc_verification_subtitle">Согласно требованиям законодательства, %@ требует пройти верификацию личности.</string>
<string name="onramp_kyc_verification_verify_button">Верифицировать</string>
<string name="onramp_kyc_verification_whats_important">Что важно знать</string>
<string name="onramp_legal">Пользуясь сервисом покупки, вы соглашаетесь с %1$s и %2$s</string> <string name="onramp_legal">Пользуясь сервисом покупки, вы соглашаетесь с %1$s и %2$s</string>
<string name="onramp_max_amount_restriction">Сумма покупки не может быть больше, чем %s</string> <string name="onramp_max_amount_restriction">Сумма покупки не может быть больше, чем %s</string>
<string name="onramp_min_amount_restriction">Сумма покупки должна составлять минимум %s</string> <string name="onramp_min_amount_restriction">Сумма покупки должна составлять минимум %s</string>
<string name="onramp_native_payment_cumulative_limit" formatted="false">Общая сумма транзакций свыше %1s может потребовать верификации личности через %2s</string>
<string name="onramp_native_payment_cumulative_limit_equivalent" formatted="false">Общая сумма транзакций, превышающая эквивалент %1s, может потребовать верификации личности через %2s</string>
<string name="onramp_native_payment_legal_notice" formatted="false">Нажимая «Оплатить», вы соглашаетесь с %1s\'s %2s и %3s.</string>
<string name="onramp_no_available_providers">Нет доступных провайдеров для выбранной валюты</string> <string name="onramp_no_available_providers">Нет доступных провайдеров для выбранной валюты</string>
<string name="onramp_offer_type_fastet">Самый быстрый</string> <string name="onramp_offer_type_fastet">Самый быстрый</string>
<string name="onramp_pay_with">Оплата с</string> <string name="onramp_pay_with">Оплата с</string>
<string name="onramp_payment_method_subtitle">Платежный метод</string> <string name="onramp_payment_method_subtitle">Платежный метод</string>
<string name="onramp_provider_max_amount">Доступно до %s</string> <string name="onramp_provider_max_amount">Доступно до %s</string>
<string name="onramp_provider_min_amount">Доступно от %s</string> <string name="onramp_provider_min_amount">Доступно от %s</string>
<string name="onramp_provider_requirements_body">Карты, выпущенные в США и Великобритании, не могут быть обработаны этим методом. Провайдер может запросить дополнительную верификацию личности</string>
<string name="onramp_provider_requirements_title">Требования провайдера</string>
<plurals name="onramp_providers_count"> <plurals name="onramp_providers_count">
<item quantity="one">%d провайдер</item> <item quantity="one">%d провайдер</item>
<item quantity="few">%d провайдера</item> <item quantity="few">%d провайдера</item>
@ -1588,6 +1600,7 @@
<string name="sui_not_enough_coin_for_fee_description">Для отправки требуется входящая транзакция на сумму не менее %1$s</string> <string name="sui_not_enough_coin_for_fee_description">Для отправки требуется входящая транзакция на сумму не менее %1$s</string>
<string name="sui_not_enough_coin_for_fee_title">Недостаточно средств</string> <string name="sui_not_enough_coin_for_fee_title">Недостаточно средств</string>
<string name="swap_approve_description">Подтверждая, вы разрешаете смарт-контракту использовать ваши токены в будущих транзакциях.</string> <string name="swap_approve_description">Подтверждая, вы разрешаете смарт-контракту использовать ваши токены в будущих транзакциях.</string>
<string name="swap_detailed_mode">Детальный режим</string>
<string name="swap_fixed_rate">Фиксированный курс</string> <string name="swap_fixed_rate">Фиксированный курс</string>
<string name="swap_give_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.</string> <string name="swap_give_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.</string>
<string name="swap_in_progress">Обмен в процессе</string> <string name="swap_in_progress">Обмен в процессе</string>
@ -1595,6 +1608,7 @@
<string name="swap_promo_title">Новый провайдер обмена!</string> <string name="swap_promo_title">Новый провайдер обмена!</string>
<string name="swap_search_tooltip_description">Найдите любой токен, даже если его ещё нет в вашем списке</string> <string name="swap_search_tooltip_description">Найдите любой токен, даже если его ещё нет в вашем списке</string>
<string name="swap_search_tooltip_title">Используйте поиск, чтобы найти то, что вам нужно.</string> <string name="swap_search_tooltip_title">Используйте поиск, чтобы найти то, что вам нужно.</string>
<string name="swap_simple_mode">Простой режим</string>
<string name="swap_story_fifth_subtitle">Чувствуйте уверенность с круглосуточной поддержкой, готовой помочь в любой ситуации!</string> <string name="swap_story_fifth_subtitle">Чувствуйте уверенность с круглосуточной поддержкой, готовой помочь в любой ситуации!</string>
<string name="swap_story_fifth_title">Круглосуточная поддержка</string> <string name="swap_story_fifth_title">Круглосуточная поддержка</string>
<string name="swap_story_first_subtitle">Надежные провайдеры в одном месте — обменивайте любые активы легко и быстро прямо в своем кошельке!</string> <string name="swap_story_first_subtitle">Надежные провайдеры в одном месте — обменивайте любые активы легко и быстро прямо в своем кошельке!</string>
@ -1632,6 +1646,10 @@
<string name="swapping_insufficient_funds">Недостаточно средств</string> <string name="swapping_insufficient_funds">Недостаточно средств</string>
<string name="swapping_insufficient_funds_description">Недостаточно средств для завершения этой транзакции. Уменьшите сумму для получения или добавьте больше средств.</string> <string name="swapping_insufficient_funds_description">Недостаточно средств для завершения этой транзакции. Уменьшите сумму для получения или добавьте больше средств.</string>
<string name="swapping_permission_header">Дать разрешение</string> <string name="swapping_permission_header">Дать разрешение</string>
<string name="swapping_rate_experience_title">Оцените ваш опыт взаимодействия с провайдером</string>
<string name="swapping_rate_feedback_placeholder">Напишите ваш отзыв</string>
<string name="swapping_rate_feedback_submit">Отправить отзыв</string>
<string name="swapping_rate_feedback_title">Что повлияло на вашу оценку?</string>
<string name="swapping_swap_action">Обменять</string> <string name="swapping_swap_action">Обменять</string>
<string name="swapping_swap_action_in_progress">Обмен…</string> <string name="swapping_swap_action_in_progress">Обмен…</string>
<string name="swapping_to_account_title">Вы получите на</string> <string name="swapping_to_account_title">Вы получите на</string>
@ -1803,6 +1821,10 @@
<string name="tangempay_tangem_visa_card">Оплачивайте ежедневные покупки в USDC</string> <string name="tangempay_tangem_visa_card">Оплачивайте ежедневные покупки в USDC</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay временно недоступен</string> <string name="tangempay_temporarily_unavailable">Tangem Pay временно недоступен</string>
<string name="tangempay_title">Tangem Pay</string> <string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">Отправьте USDC Polygon на адрес вашего аккаунта</string>
<string name="tangempay_topup_receive_title">С другого кошелька или биржи</string>
<string name="tangempay_topup_swap_body">Обменяйте любой актив на USDC Polygon</string>
<string name="tangempay_topup_swap_title">Из вашего кошелька Tangem</string>
<string name="tangempay_usdc_on_polygon_network">USDC в сети Polygon</string> <string name="tangempay_usdc_on_polygon_network">USDC в сети Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Нажмите на кнопку ниже, чтобы восстановить доступ</string> <string name="tangempay_use_tangem_device_to_restore_payment_account">Нажмите на кнопку ниже, чтобы восстановить доступ</string>
<string name="tangempay_withdrawal_note_description">При возвратах покупок средства не возвращаются на ончейн-баланс Polygon и недоступны для вывода, но отображаются на карте и могут быть использованы для покупок</string> <string name="tangempay_withdrawal_note_description">При возвратах покупок средства не возвращаются на ончейн-баланс Polygon и недоступны для вывода, но отображаются на карте и могут быть использованы для покупок</string>

View file

@ -80,6 +80,7 @@
<string name="action_buttons_swap_not_enough_tokens_alert_title">Додайте токени</string> <string name="action_buttons_swap_not_enough_tokens_alert_title">Додайте токени</string>
<string name="action_buttons_you_want_to_receive">Оберіть токен для отримання</string> <string name="action_buttons_you_want_to_receive">Оберіть токен для отримання</string>
<string name="action_buttons_you_want_to_swap">Оберіть токен для обміну</string> <string name="action_buttons_you_want_to_swap">Оберіть токен для обміну</string>
<string name="add_and_manage_sheet_manage_title">Додати токени</string>
<string name="add_custom_token_choose_network">Оберіть мережу</string> <string name="add_custom_token_choose_network">Оберіть мережу</string>
<string name="add_custom_token_title">Додати токен</string> <string name="add_custom_token_title">Додати токен</string>
<string name="add_tokens_title">Токени</string> <string name="add_tokens_title">Токени</string>
@ -569,6 +570,7 @@
<string name="express_provider">Провайдер</string> <string name="express_provider">Провайдер</string>
<string name="express_provider_best_rate">Найкращий курс</string> <string name="express_provider_best_rate">Найкращий курс</string>
<string name="express_provider_fca_warning_list">Список попереджень FCA</string> <string name="express_provider_fca_warning_list">Список попереджень FCA</string>
<string name="express_provider_for_swap">Провайдер для обміну</string>
<string name="express_provider_great_rate">Найкращий вибір</string> <string name="express_provider_great_rate">Найкращий вибір</string>
<string name="express_provider_in_fca_warning_list">Список попереджень FCA</string> <string name="express_provider_in_fca_warning_list">Список попереджень FCA</string>
<string name="express_provider_max_amount">Доступно до %s</string> <string name="express_provider_max_amount">Доступно до %s</string>
@ -731,6 +733,7 @@
<string name="koinos_mana_exceeds_koin_balance_title">Ліміт Mana</string> <string name="koinos_mana_exceeds_koin_balance_title">Ліміт Mana</string>
<string name="koinos_mana_level_description">Мережа Koinos вимагає Mana для мережевої комісії. У вас є %1$s/%2$s Mana</string> <string name="koinos_mana_level_description">Мережа Koinos вимагає Mana для мережевої комісії. У вас є %1$s/%2$s Mana</string>
<string name="koinos_mana_level_title">Рівень Mana</string> <string name="koinos_mana_level_title">Рівень Mana</string>
<string name="main_add_and_manage_tokens">Додати та керувати</string>
<string name="main_empty_tokens_list_message">Щоб почати відстежувати свої криптоактиви та транзакції, додайте токени</string> <string name="main_empty_tokens_list_message">Щоб почати відстежувати свої криптоактиви та транзакції, додайте токени</string>
<string name="main_manage_tokens">Керування токенами</string> <string name="main_manage_tokens">Керування токенами</string>
<string name="main_scan_card_warning_view_subtitle">Для доступу до всіх мереж необхідно відсканувати картку</string> <string name="main_scan_card_warning_view_subtitle">Для доступу до всіх мереж необхідно відсканувати картку</string>
@ -1092,16 +1095,27 @@
<string name="onramp_error_transaction_already_processed">Ця транзакція вже була оброблена. Додаткові дії не потребуються.</string> <string name="onramp_error_transaction_already_processed">Ця транзакція вже була оброблена. Додаткові дії не потребуються.</string>
<string name="onramp_fetching_best_rates">Шукаємо найвигідніший курс...</string> <string name="onramp_fetching_best_rates">Шукаємо найвигідніший курс...</string>
<string name="onramp_instant_status">Миттєво</string> <string name="onramp_instant_status">Миттєво</string>
<string name="onramp_kyc_verification_bullet_free">Верифікація безкоштовна і зазвичай займає 1-2 хвилини</string>
<string name="onramp_kyc_verification_bullet_privacy">Tangem не матиме доступу до ваших особистих даних, ви передаєте їх безпосередньо ліцензованому провайдеру</string>
<string name="onramp_kyc_verification_choose_another">Виберіть інший метод</string>
<string name="onramp_kyc_verification_subtitle">Згідно з вимогами законодавства, %@ вимагає пройти верифікацію особи.</string>
<string name="onramp_kyc_verification_verify_button">Верифікувати</string>
<string name="onramp_kyc_verification_whats_important">Що важливо знати</string>
<string name="onramp_legal">Використовуючи сервіс покупки, ви погоджуєтесь з %1$s та %2$s</string> <string name="onramp_legal">Використовуючи сервіс покупки, ви погоджуєтесь з %1$s та %2$s</string>
<string name="onramp_legal_text">Послуга надається зовнішнім провайдером.\nTangem не несе відповідальності.</string> <string name="onramp_legal_text">Послуга надається зовнішнім провайдером.\nTangem не несе відповідальності.</string>
<string name="onramp_max_amount_restriction">Сума покупки не може бути більше ніж %s</string> <string name="onramp_max_amount_restriction">Сума покупки не може бути більше ніж %s</string>
<string name="onramp_min_amount_restriction">Сума покупки повинна бути не менше %s</string> <string name="onramp_min_amount_restriction">Сума покупки повинна бути не менше %s</string>
<string name="onramp_native_payment_cumulative_limit" formatted="false">Загальна сума транзакцій понад %1s може вимагати верифікації особи через %2s</string>
<string name="onramp_native_payment_cumulative_limit_equivalent" formatted="false">Загальна сума транзакцій, що перевищує еквівалент %1s, може вимагати верифікації особи через %2s</string>
<string name="onramp_native_payment_legal_notice" formatted="false">Натискаючи «Оплатити», ви погоджуєтеся з %1s\'s %2s і %3s.</string>
<string name="onramp_no_available_providers">Для данної валюти немає доступних провайдерів</string> <string name="onramp_no_available_providers">Для данної валюти немає доступних провайдерів</string>
<string name="onramp_offer_type_fastet">Найшвидший</string> <string name="onramp_offer_type_fastet">Найшвидший</string>
<string name="onramp_pay_with">Оплата з</string> <string name="onramp_pay_with">Оплата з</string>
<string name="onramp_payment_method_subtitle">Спосіб оплати</string> <string name="onramp_payment_method_subtitle">Спосіб оплати</string>
<string name="onramp_provider_max_amount">Доступно до %s</string> <string name="onramp_provider_max_amount">Доступно до %s</string>
<string name="onramp_provider_min_amount">Доступно від %s</string> <string name="onramp_provider_min_amount">Доступно від %s</string>
<string name="onramp_provider_requirements_body">Картки, випущені у США та Великій Британії, не можуть бути оброблені цим методом. Провайдер може запросити додаткову верифікацію особи</string>
<string name="onramp_provider_requirements_title">Вимоги провайдера</string>
<plurals name="onramp_providers_count"> <plurals name="onramp_providers_count">
<item quantity="one">%d провайдер</item> <item quantity="one">%d провайдер</item>
<item quantity="few">%d провайдери</item> <item quantity="few">%d провайдери</item>
@ -1509,6 +1523,7 @@
<string name="sui_not_enough_coin_for_fee_description">Для відправки потрібна вхідна транзакція на суму не менше %1$s</string> <string name="sui_not_enough_coin_for_fee_description">Для відправки потрібна вхідна транзакція на суму не менше %1$s</string>
<string name="sui_not_enough_coin_for_fee_title">Недостатньо коштів</string> <string name="sui_not_enough_coin_for_fee_title">Недостатньо коштів</string>
<string name="swap_approve_description">Підтверджуючи, ви дозволяєте смартконтракту використовувати ваші токени в майбутніх транзакціях.</string> <string name="swap_approve_description">Підтверджуючи, ви дозволяєте смартконтракту використовувати ваші токени в майбутніх транзакціях.</string>
<string name="swap_detailed_mode">Детальний режим</string>
<string name="swap_fixed_rate">Фіксований курс</string> <string name="swap_fixed_rate">Фіксований курс</string>
<string name="swap_give_permission_fee_footer">Мережа стягує комісію за схвалення токену за підтвердження, що саме ви дозволяєте використовувати ваш токен для обміну.</string> <string name="swap_give_permission_fee_footer">Мережа стягує комісію за схвалення токену за підтвердження, що саме ви дозволяєте використовувати ваш токен для обміну.</string>
<string name="swap_in_progress">Обмін у процесі</string> <string name="swap_in_progress">Обмін у процесі</string>
@ -1517,6 +1532,7 @@
<string name="swap_search_suggestion_hint">Шукаєте щось інше?\nСпробуйте пошукати або перегляньте інші криптовалюти!</string> <string name="swap_search_suggestion_hint">Шукаєте щось інше?\nСпробуйте пошукати або перегляньте інші криптовалюти!</string>
<string name="swap_search_tooltip_description">Шукайте будь-який токен, навіть якщо його ще немає у вашому списку.</string> <string name="swap_search_tooltip_description">Шукайте будь-який токен, навіть якщо його ще немає у вашому списку.</string>
<string name="swap_search_tooltip_title">Використовуйте пошук, щоб знайти потрібне</string> <string name="swap_search_tooltip_title">Використовуйте пошук, щоб знайти потрібне</string>
<string name="swap_simple_mode">Простий режим</string>
<string name="swap_story_fifth_subtitle">Надійна підтримка 24/7, щоб ваші фінансові операції були швидкими та надійними!</string> <string name="swap_story_fifth_subtitle">Надійна підтримка 24/7, щоб ваші фінансові операції були швидкими та надійними!</string>
<string name="swap_story_fifth_title">Цілодобова підтримка</string> <string name="swap_story_fifth_title">Цілодобова підтримка</string>
<string name="swap_story_first_subtitle">Надійні провайдери дозволяють легко обмінювати активи, забезпечуючи повну безпеку у вашому гаманці</string> <string name="swap_story_first_subtitle">Надійні провайдери дозволяють легко обмінювати активи, забезпечуючи повну безпеку у вашому гаманці</string>
@ -1551,6 +1567,10 @@
<string name="swapping_high_price_impact_title">Високий вплив на ціну</string> <string name="swapping_high_price_impact_title">Високий вплив на ціну</string>
<string name="swapping_insufficient_funds">Недостатньо коштів</string> <string name="swapping_insufficient_funds">Недостатньо коштів</string>
<string name="swapping_permission_header">Надати дозвіл</string> <string name="swapping_permission_header">Надати дозвіл</string>
<string name="swapping_rate_experience_title">Оцініть ваш досвід взаємодії з провайдером</string>
<string name="swapping_rate_feedback_placeholder">Напишіть ваш відгук</string>
<string name="swapping_rate_feedback_submit">Надіслати відгук</string>
<string name="swapping_rate_feedback_title">Що вплинуло на вашу оцінку?</string>
<string name="swapping_swap_action">Обміняти</string> <string name="swapping_swap_action">Обміняти</string>
<string name="swapping_swap_action_in_progress">Обмін...</string> <string name="swapping_swap_action_in_progress">Обмін...</string>
<string name="swapping_to_account_title">Ви отримаєте на</string> <string name="swapping_to_account_title">Ви отримаєте на</string>
@ -1720,6 +1740,10 @@
<string name="tangempay_tangem_visa_card">Використовуйте USDC для щоденних платежів</string> <string name="tangempay_tangem_visa_card">Використовуйте USDC для щоденних платежів</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay тимчасово недоступний</string> <string name="tangempay_temporarily_unavailable">Tangem Pay тимчасово недоступний</string>
<string name="tangempay_title">Tangem Pay</string> <string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">Надішліть USDC Polygon на адресу вашого акаунту</string>
<string name="tangempay_topup_receive_title">З іншого гаманця або біржі</string>
<string name="tangempay_topup_swap_body">Обміняйте будь-який актив на USDC Polygon</string>
<string name="tangempay_topup_swap_title">З вашого Tangem Wallet</string>
<string name="tangempay_usdc_on_polygon_network">USDC у Polygon</string> <string name="tangempay_usdc_on_polygon_network">USDC у Polygon</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">Натисніть кнопку нижче, щоб відновити доступ</string> <string name="tangempay_use_tangem_device_to_restore_payment_account">Натисніть кнопку нижче, щоб відновити доступ</string>
<string name="tangempay_withdrawal_note_description">Кошти з повернених покупок не будуть повернуті на ваш ончейн-баланс Polygon і не будуть доступні для виведення, але залишаться на балансі картки для покупок.</string> <string name="tangempay_withdrawal_note_description">Кошти з повернених покупок не будуть повернуті на ваш ончейн-баланс Polygon і не будуть доступні для виведення, але залишаться на балансі картки для покупок.</string>

View file

@ -1187,7 +1187,9 @@
<string name="organize_tokens_title">整理代币</string> <string name="organize_tokens_title">整理代币</string>
<string name="organize_tokens_ungroup">取消分组</string> <string name="organize_tokens_ungroup">取消分组</string>
<string name="provider_name_support">%s 支持</string> <string name="provider_name_support">%s 支持</string>
<string name="push_notification_settings_banner_button_grant_permission">授予权限</string>
<string name="push_notification_settings_banner_description">推送通知已启用,但需要您在设备设置中允许通知才能正常工作。</string> <string name="push_notification_settings_banner_description">推送通知已启用,但需要您在设备设置中允许通知才能正常工作。</string>
<string name="push_notification_settings_banner_description_grant_permission">推送通知已启用,但需要您授予权限才能生效。</string>
<string name="push_notification_settings_banner_title">允许通知</string> <string name="push_notification_settings_banner_title">允许通知</string>
<string name="push_notification_settings_offers_updates_subtitle">产品资讯、独家优惠和活动提醒。</string> <string name="push_notification_settings_offers_updates_subtitle">产品资讯、独家优惠和活动提醒。</string>
<string name="push_notification_settings_offers_updates_title">优惠与更新</string> <string name="push_notification_settings_offers_updates_title">优惠与更新</string>
@ -1827,6 +1829,10 @@
<string name="tangempay_tangem_visa_card">使用 USDC 进行日常支付</string> <string name="tangempay_tangem_visa_card">使用 USDC 进行日常支付</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay暂时无法使用。</string> <string name="tangempay_temporarily_unavailable">Tangem Pay暂时无法使用。</string>
<string name="tangempay_title">Tangem Pay</string> <string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">將 USDC Polygon 發送至您帳戶地址</string>
<string name="tangempay_topup_receive_title">從其他錢包或交易所</string>
<string name="tangempay_topup_swap_body">將任何資產兌換為 USDC Polygon</string>
<string name="tangempay_topup_swap_title">從您的 Tangem 錢包</string>
<string name="tangempay_usdc_on_polygon_network">Polygon网络上的 USDC</string> <string name="tangempay_usdc_on_polygon_network">Polygon网络上的 USDC</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">点击下方按钮恢复访问权限</string> <string name="tangempay_use_tangem_device_to_restore_payment_account">点击下方按钮恢复访问权限</string>
<string name="tangempay_withdrawal_note_description">您的Polygon链上 USDC 余额与您的卡片余额不同,并在购买后 2 个工作日内更新。购物退款的资金不会退还至您的链上余额,也不能提现,但会保留在您的卡片余额中用于购物。</string> <string name="tangempay_withdrawal_note_description">您的Polygon链上 USDC 余额与您的卡片余额不同,并在购买后 2 个工作日内更新。购物退款的资金不会退还至您的链上余额,也不能提现,但会保留在您的卡片余额中用于购物。</string>

View file

@ -435,6 +435,10 @@
<string name="tangempay_tangem_visa_card">使用 USDC 進行日常支付</string> <string name="tangempay_tangem_visa_card">使用 USDC 進行日常支付</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay暂时不可用</string> <string name="tangempay_temporarily_unavailable">Tangem Pay暂时不可用</string>
<string name="tangempay_title">Tangem Pay</string> <string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">将 USDC Polygon 发送至您账户地址</string>
<string name="tangempay_topup_receive_title">从其他钱包或交易所</string>
<string name="tangempay_topup_swap_body">将任何资产兑换为 USDC Polygon</string>
<string name="tangempay_topup_swap_title">从您的 Tangem 钱包</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">點擊下方按鈕以恢復存取權限</string> <string name="tangempay_use_tangem_device_to_restore_payment_account">點擊下方按鈕以恢復存取權限</string>
<string name="tangempay_withdrawal_note_description">您的 USDC Polygon 鏈上餘額與卡片餘額不同,並在購買後 2 個工作日內更新。退款交易的資金不會返回到您的鏈上餘額或可供提現,但會保留在您的卡片餘額中用於購買。</string> <string name="tangempay_withdrawal_note_description">您的 USDC Polygon 鏈上餘額與卡片餘額不同,並在購買後 2 個工作日內更新。退款交易的資金不會返回到您的鏈上餘額或可供提現,但會保留在您的卡片餘額中用於購買。</string>
<string name="tangempay_withdrawal_note_title">請注意</string> <string name="tangempay_withdrawal_note_title">請注意</string>

View file

@ -31,6 +31,7 @@ import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.* import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import androidx.compose.ui.text.AnnotatedString
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
@ -259,7 +260,9 @@ internal fun TextsBlock(
titleColor: Color = TangemTheme.colors.text.primary1, titleColor: Color = TangemTheme.colors.text.primary1,
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
val titleText = title?.resolveReference() val titleText = title?.let { ref ->
if (ref is TextReference.Annotated) ref.value else AnnotatedString(ref.resolveReference())
}
if (titleText != null) { if (titleText != null) {
Text( Text(

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18.527,6.25C18.971,6.25 19.345,6.25 19.649,6.272C19.965,6.295 20.272,6.345 20.565,6.48C20.896,6.633 21.192,6.862 21.407,7.161C21.611,7.443 21.687,7.747 21.72,8.04C21.75,8.304 21.75,8.621 21.75,8.964V9.036C21.75,9.379 21.75,9.696 21.72,9.96C21.687,10.252 21.611,10.557 21.407,10.839C21.192,11.138 20.896,11.367 20.565,11.52C20.272,11.655 19.965,11.705 19.649,11.728C19.345,11.75 18.971,11.75 18.527,11.75H5.473C5.029,11.75 4.655,11.75 4.351,11.728C4.035,11.705 3.728,11.655 3.436,11.52C3.104,11.367 2.808,11.138 2.593,10.839C2.389,10.557 2.313,10.252 2.28,9.96C2.25,9.696 2.25,9.379 2.25,9.036V8.964C2.25,8.621 2.25,8.304 2.28,8.04C2.313,7.747 2.389,7.443 2.593,7.161C2.808,6.862 3.104,6.633 3.436,6.48C3.728,6.345 4.035,6.295 4.351,6.272C4.655,6.25 5.029,6.25 5.473,6.25H18.527Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M3.787,12.918C3.523,12.874 3.392,12.852 3.321,12.911C3.25,12.971 3.25,13.094 3.25,13.34V15.054C3.25,16.657 3.25,17.936 3.385,18.939C3.524,19.975 3.82,20.829 4.495,21.504C5.17,22.179 6.023,22.474 7.06,22.614C8.063,22.749 9.342,22.749 10.944,22.749C10.975,22.749 11,22.724 11,22.693L11,13.399C11,13.21 11,13.116 10.941,13.057C10.883,12.999 10.789,12.999 10.6,12.999H5.439C5.022,12.999 4.611,12.999 4.26,12.973C4.115,12.963 3.956,12.947 3.787,12.918Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M13.4,12.999C13.211,12.999 13.117,12.999 13.059,13.057C13,13.116 13,13.21 13,13.399V22.693C13,22.724 13.025,22.749 13.056,22.749C14.658,22.749 15.937,22.749 16.94,22.614C17.977,22.474 18.83,22.179 19.505,21.504C20.18,20.829 20.476,19.975 20.615,18.939C20.75,17.936 20.75,16.657 20.75,15.054V13.34C20.75,13.094 20.75,12.971 20.679,12.911C20.608,12.852 20.477,12.874 20.213,12.918C20.044,12.947 19.885,12.963 19.74,12.973C19.389,12.999 18.978,12.999 18.561,12.999H13.4Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M8.143,1.25C9.715,1.25 11.112,1.998 12,3.156C12.888,1.998 14.285,1.25 15.857,1.25H16.214C17.752,1.25 19,2.498 19,4.036C19,6.363 17.113,8.25 14.786,8.25H9.214C6.887,8.25 5,6.363 5,4.036C5,2.498 6.248,1.25 7.786,1.25H8.143ZM7.786,3.25C7.352,3.25 7,3.602 7,4.036C7,5.259 7.991,6.25 9.214,6.25H11V6.107C11,4.529 9.721,3.25 8.143,3.25H7.786ZM15.857,3.25C14.28,3.25 13,4.529 13,6.107V6.25H14.786C16.009,6.25 17,5.259 17,4.036C17,3.602 16.648,3.25 16.214,3.25H15.857Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -43,6 +43,7 @@ dependencies {
kapt(deps.hilt.kapt) kapt(deps.hilt.kapt)
/** Other */ /** Other */
implementation(deps.kotlin.datetime)
/** tests */ /** tests */
testImplementation(projects.common.test) testImplementation(projects.common.test)

View file

@ -4,13 +4,18 @@ import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.data.yield.supply.DefaultYieldSupplyRepository import com.tangem.data.yield.supply.DefaultYieldSupplyRepository
import com.tangem.data.yield.supply.DefaultYieldSupplyErrorResolver import com.tangem.data.yield.supply.DefaultYieldSupplyErrorResolver
import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository
import com.tangem.data.yield.supply.promo.DefaultYieldPromoRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.YieldSupplyApi import com.tangem.datasource.api.tangemTech.YieldSupplyApi
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.datasource.local.yieldsupply.promo.YieldBoostPromoStore
import com.tangem.datasource.local.yieldsupply.promo.YieldBoostStatusStore
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
import com.tangem.domain.yield.supply.promo.YieldPromoRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -59,4 +64,20 @@ internal object YieldSupplyDataModule {
fun provideYieldSupplyErrorResolver(): YieldSupplyErrorResolver { fun provideYieldSupplyErrorResolver(): YieldSupplyErrorResolver {
return DefaultYieldSupplyErrorResolver return DefaultYieldSupplyErrorResolver
} }
@Provides
@Singleton
fun provideYieldPromoRepository(
tangemApi: TangemTechApi,
promoStore: YieldBoostPromoStore,
statusStore: YieldBoostStatusStore,
dispatchers: CoroutineDispatcherProvider,
): YieldPromoRepository {
return DefaultYieldPromoRepository(
tangemApi = tangemApi,
promoStore = promoStore,
statusStore = statusStore,
dispatchers = dispatchers,
)
}
} }

View file

@ -0,0 +1,63 @@
package com.tangem.data.yield.supply.promo
import com.tangem.data.yield.supply.promo.converter.YieldBoostPromoConverter
import com.tangem.data.yield.supply.promo.converter.YieldBoostStatusConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.yieldsupply.promo.YieldBoostPromoStore
import com.tangem.datasource.local.yieldsupply.promo.YieldBoostStatusStore
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldBoostPromo
import com.tangem.domain.yield.supply.models.YieldBoostStatus
import com.tangem.domain.yield.supply.promo.YieldPromoRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class DefaultYieldPromoRepository(
private val tangemApi: TangemTechApi,
private val promoStore: YieldBoostPromoStore,
private val statusStore: YieldBoostStatusStore,
private val dispatchers: CoroutineDispatcherProvider,
) : YieldPromoRepository {
override suspend fun getYieldBoostPromo(userWalletId: UserWalletId, forceRefresh: Boolean): YieldBoostPromo {
if (!forceRefresh) {
promoStore.getSyncOrNull(userWalletId)?.let { return it }
}
return try {
val fresh = fetchPromo(userWalletId)
promoStore.store(userWalletId, fresh)
fresh
} catch (e: Exception) {
promoStore.getSyncOrNull(userWalletId) ?: throw e
}
}
override suspend fun getYieldBoostStatus(userWalletId: UserWalletId, forceRefresh: Boolean): YieldBoostStatus {
if (!forceRefresh) {
statusStore.getSyncOrNull(userWalletId)?.let { return it }
}
return try {
val fresh = fetchStatus(userWalletId)
statusStore.store(userWalletId, fresh)
fresh
} catch (e: Exception) {
statusStore.getSyncOrNull(userWalletId) ?: throw e
}
}
private suspend fun fetchPromo(userWalletId: UserWalletId): YieldBoostPromo = withContext(dispatchers.io) {
val response = tangemApi.getPromotions(walletId = userWalletId.stringValue).getOrThrow()
val dto = response.promotions.firstOrNull { it.name == PROMO_NAME } ?: return@withContext YieldBoostPromo.None
YieldBoostPromoConverter.convert(dto)
}
private suspend fun fetchStatus(userWalletId: UserWalletId): YieldBoostStatus = withContext(dispatchers.io) {
val response = tangemApi.getYieldBoostStatus(walletId = userWalletId.stringValue).getOrThrow()
YieldBoostStatusConverter.convert(response)
}
private companion object {
const val PROMO_NAME = "yield-apr-boost"
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.data.yield.supply.promo.converter
import com.tangem.datasource.api.promotion.models.PromotionsResponse
import com.tangem.domain.yield.supply.models.YieldBoostPromo
import kotlinx.datetime.Instant
internal object YieldBoostPromoConverter {
private const val ACTIVE_STATUS = "active"
fun convert(dto: PromotionsResponse.PromotionDto): YieldBoostPromo {
val all = dto.all ?: return YieldBoostPromo.None
if (!all.status.equals(ACTIVE_STATUS, ignoreCase = true)) return YieldBoostPromo.None
val start = runCatching { Instant.parse(all.timeline.start) }.getOrNull() ?: return YieldBoostPromo.None
val end = runCatching { Instant.parse(all.timeline.end) }.getOrNull() ?: return YieldBoostPromo.None
val tokens = all.tokens.orEmpty().map { token ->
YieldBoostPromo.Active.PromoToken(
contractAddress = token.tokenAddress,
tokenSymbol = token.tokenSymbol,
tokenName = token.tokenName,
networkId = token.networkId,
)
}
if (tokens.isEmpty()) return YieldBoostPromo.None
return YieldBoostPromo.Active(
tokens = tokens,
timeline = YieldBoostPromo.Active.Timeline(start = start, end = end),
link = all.link,
)
}
}

View file

@ -0,0 +1,63 @@
package com.tangem.data.yield.supply.promo.converter
import com.tangem.datasource.api.promotion.models.YieldBoostStatusResponse
import com.tangem.domain.yield.supply.models.YieldBoostStatus
import kotlinx.datetime.Instant
internal object YieldBoostStatusConverter {
private const val STATUS_NOT_STARTED = "notstarted"
private const val STATUS_ACTIVE = "active"
private const val STATUS_COMPLETED = "completed"
private const val STATUS_DISQUALIFIED = "disqualified"
private const val REASON_FROD = "frod"
private const val REASON_LESS_THAN_1_USD = "less1usd"
private const val REASON_CLOSED = "closed"
fun convert(dto: YieldBoostStatusResponse): YieldBoostStatus = when (dto.promoEnrollmentStatus.lowercase()) {
STATUS_ACTIVE -> dto.toActive() ?: YieldBoostStatus.NotStarted
STATUS_COMPLETED -> dto.toCompleted() ?: YieldBoostStatus.NotStarted
STATUS_DISQUALIFIED -> YieldBoostStatus.Disqualified(reason = dto.disqualificationReason.toReason())
STATUS_NOT_STARTED -> YieldBoostStatus.NotStarted
else -> YieldBoostStatus.NotStarted // forward-compat: unknown status → treat as NotStarted
}
/** Backend `"active"` → [YieldBoostStatus.Active]. Returns `null` if mandatory dates can't be parsed. */
private fun YieldBoostStatusResponse.toActive(): YieldBoostStatus.Active? {
val activation = activationDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null
val qualificationEnd =
qualificationEndDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null
return YieldBoostStatus.Active(
tokenName = tokenName.orEmpty(),
networkId = networkId.orEmpty(),
moduleAddress = moduleAddress.orEmpty(),
userAddress = userAddress.orEmpty(),
contractAddress = contractAddress.orEmpty(),
activationDate = activation,
qualificationEndDate = qualificationEnd,
)
}
private fun YieldBoostStatusResponse.toCompleted(): YieldBoostStatus.Completed? {
val activation = activationDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null
val qualificationEnd =
qualificationEndDate?.let { runCatching { Instant.parse(it) }.getOrNull() } ?: return null
return YieldBoostStatus.Completed(
tokenName = tokenName.orEmpty(),
networkId = networkId.orEmpty(),
moduleAddress = moduleAddress.orEmpty(),
userAddress = userAddress.orEmpty(),
contractAddress = contractAddress.orEmpty(),
activationDate = activation,
qualificationEndDate = qualificationEnd,
)
}
private fun String?.toReason(): YieldBoostStatus.Disqualified.Reason = when (this?.lowercase()) {
REASON_FROD -> YieldBoostStatus.Disqualified.Reason.FROD
REASON_LESS_THAN_1_USD -> YieldBoostStatus.Disqualified.Reason.LESS_THAN_1_USD
REASON_CLOSED -> YieldBoostStatus.Disqualified.Reason.CLOSED
else -> YieldBoostStatus.Disqualified.Reason.UNKNOWN
}
}

View file

@ -0,0 +1,119 @@
package com.tangem.data.yield.supply.promo.converter
import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.api.promotion.models.PromotionsResponse
import com.tangem.domain.yield.supply.models.YieldBoostPromo
import org.junit.jupiter.api.Test
class YieldBoostPromoConverterTest {
@Test
fun `GIVEN active dto with tokens WHEN convert THEN returns Active`() {
val dto = activeDto()
val result = YieldBoostPromoConverter.convert(dto)
assertThat(result).isInstanceOf(YieldBoostPromo.Active::class.java)
val active = result as YieldBoostPromo.Active
assertThat(active.tokens).hasSize(2)
assertThat(active.tokens.first().contractAddress)
.isEqualTo("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
assertThat(active.tokens.first().networkId).isEqualTo("ethereum")
assertThat(active.link).isEqualTo("https://example.com/terms")
}
@Test
fun `GIVEN dto with null all WHEN convert THEN returns None`() {
val dto = PromotionsResponse.PromotionDto(name = "promo-yield-apr-boost", all = null)
val result = YieldBoostPromoConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostPromo.None)
}
@Test
fun `GIVEN dto with non-active status WHEN convert THEN returns None`() {
val dto = activeDto().copy(
all = activeDto().all!!.copy(status = "expired"),
)
val result = YieldBoostPromoConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostPromo.None)
}
@Test
fun `GIVEN dto with empty tokens WHEN convert THEN returns None`() {
val dto = activeDto().copy(
all = activeDto().all!!.copy(tokens = emptyList()),
)
val result = YieldBoostPromoConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostPromo.None)
}
@Test
fun `GIVEN dto with null tokens WHEN convert THEN returns None`() {
val dto = activeDto().copy(
all = activeDto().all!!.copy(tokens = null),
)
val result = YieldBoostPromoConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostPromo.None)
}
@Test
fun `GIVEN dto with malformed start date WHEN convert THEN returns None`() {
val dto = activeDto().copy(
all = activeDto().all!!.copy(
timeline = PromotionsResponse.PromotionDto.Timeline(
start = "not-an-iso",
end = "2027-06-15T22:00:00.000Z",
),
),
)
val result = YieldBoostPromoConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostPromo.None)
}
@Test
fun `GIVEN status with uppercase casing WHEN convert THEN treats as active`() {
val dto = activeDto().copy(
all = activeDto().all!!.copy(status = "ACTIVE"),
)
val result = YieldBoostPromoConverter.convert(dto)
assertThat(result).isInstanceOf(YieldBoostPromo.Active::class.java)
}
private fun activeDto() = PromotionsResponse.PromotionDto(
name = "promo-yield-apr-boost",
all = PromotionsResponse.PromotionDto.All(
timeline = PromotionsResponse.PromotionDto.Timeline(
start = "2026-06-15T00:00:00.000Z",
end = "2027-06-15T22:00:00.000Z",
),
tokens = listOf(
PromotionsResponse.PromotionDto.PromoToken(
tokenAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
tokenSymbol = "USDC",
tokenName = "USD Coin",
networkId = "ethereum",
),
PromotionsResponse.PromotionDto.PromoToken(
tokenAddress = "0xdac17f958d2ee523a2206206994597c13d831ec7",
tokenSymbol = "USDT",
tokenName = "Tether USD",
networkId = "ethereum",
),
),
status = "active",
link = "https://example.com/terms",
),
)
}

View file

@ -0,0 +1,186 @@
package com.tangem.data.yield.supply.promo.converter
import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.api.promotion.models.YieldBoostStatusResponse
import com.tangem.domain.yield.supply.models.YieldBoostStatus
import org.junit.jupiter.api.Test
class YieldBoostStatusConverterTest {
private val activation = "2026-05-01T00:00:00Z"
private val qualificationEnd = "2026-06-01T00:00:00Z"
@Test
fun `GIVEN promoEnrollmentStatus notStarted WHEN convert THEN returns NotStarted`() {
val dto = dto(promoEnrollmentStatus = "notStarted")
val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostStatus.NotStarted)
}
@Test
fun `GIVEN active backend status with valid dates WHEN convert THEN returns Active`() {
val dto = dto(
promoEnrollmentStatus = "active",
tokenName = "USD Coin",
networkId = "ethereum",
moduleAddress = "0xmodule",
userAddress = "0xuser",
contractAddress = "0xcontract",
activationDate = activation,
qualificationEndDate = qualificationEnd,
)
val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isInstanceOf(YieldBoostStatus.Active::class.java)
val active = result as YieldBoostStatus.Active
assertThat(active.tokenName).isEqualTo("USD Coin")
assertThat(active.networkId).isEqualTo("ethereum")
assertThat(active.contractAddress).isEqualTo("0xcontract")
}
@Test
fun `GIVEN active status missing activationDate WHEN convert THEN falls back to NotStarted`() {
val dto = dto(
promoEnrollmentStatus = "active",
activationDate = null,
qualificationEndDate = qualificationEnd,
)
val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostStatus.NotStarted)
}
@Test
fun `GIVEN active status with malformed activationDate WHEN convert THEN falls back to NotStarted`() {
val dto = dto(
promoEnrollmentStatus = "active",
activationDate = "not-an-iso",
qualificationEndDate = qualificationEnd,
)
val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostStatus.NotStarted)
}
@Test
fun `GIVEN completed status with valid dates WHEN convert THEN returns Completed`() {
val dto = dto(
promoEnrollmentStatus = "completed",
tokenName = "USDT",
networkId = "ethereum",
moduleAddress = "0xmodule",
userAddress = "0xuser",
contractAddress = "0xcontract",
activationDate = "2026-04-01T00:00:00Z",
qualificationEndDate = "2026-05-01T00:00:00Z",
)
val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isInstanceOf(YieldBoostStatus.Completed::class.java)
}
@Test
fun `GIVEN disqualified frod reason WHEN convert THEN returns Disqualified with FROD reason`() {
val dto = dto(
promoEnrollmentStatus = "disqualified",
disqualificationReason = "frod",
)
val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostStatus.Disqualified(YieldBoostStatus.Disqualified.Reason.FROD))
}
@Test
fun `GIVEN disqualified less1usd reason WHEN convert THEN returns Disqualified with LESS_THAN_1_USD reason`() {
val dto = dto(
promoEnrollmentStatus = "disqualified",
disqualificationReason = "less1usd",
)
val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isEqualTo(
YieldBoostStatus.Disqualified(YieldBoostStatus.Disqualified.Reason.LESS_THAN_1_USD),
)
}
@Test
fun `GIVEN disqualified closed reason WHEN convert THEN returns Disqualified with CLOSED reason`() {
val dto = dto(
promoEnrollmentStatus = "disqualified",
disqualificationReason = "closed",
)
val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostStatus.Disqualified(YieldBoostStatus.Disqualified.Reason.CLOSED))
}
@Test
fun `GIVEN disqualified unknown reason WHEN convert THEN returns Disqualified with UNKNOWN reason`() {
val dto = dto(
promoEnrollmentStatus = "disqualified",
disqualificationReason = "alien_invasion",
)
val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostStatus.Disqualified(YieldBoostStatus.Disqualified.Reason.UNKNOWN))
}
@Test
fun `GIVEN unknown promoEnrollmentStatus WHEN convert THEN returns NotStarted`() {
val dto = dto(promoEnrollmentStatus = "futureBackendStatus")
val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isEqualTo(YieldBoostStatus.NotStarted)
}
@Test
fun `GIVEN status with uppercase casing WHEN convert THEN normalizes correctly`() {
val dto = dto(
promoEnrollmentStatus = "ACTIVE",
tokenName = "USDT",
networkId = "ethereum",
moduleAddress = "0xmodule",
userAddress = "0xuser",
contractAddress = "0xcontract",
activationDate = activation,
qualificationEndDate = qualificationEnd,
)
val result = YieldBoostStatusConverter.convert(dto)
assertThat(result).isInstanceOf(YieldBoostStatus.Active::class.java)
}
private fun dto(
promoEnrollmentStatus: String,
tokenName: String? = null,
networkId: String? = null,
moduleAddress: String? = null,
userAddress: String? = null,
contractAddress: String? = null,
activationDate: String? = null,
qualificationEndDate: String? = null,
disqualificationReason: String? = null,
) = YieldBoostStatusResponse(
tokenName = tokenName,
networkId = networkId,
moduleAddress = moduleAddress,
userAddress = userAddress,
contractAddress = contractAddress,
promoEnrollmentStatus = promoEnrollmentStatus,
activationDate = activationDate,
qualificationEndDate = qualificationEndDate,
disqualificationReason = disqualificationReason,
)
}

View file

@ -25,4 +25,5 @@ data class StoryContent(
enum class StoryContentIds(val id: String, val analyticType: String) { enum class StoryContentIds(val id: String, val analyticType: String) {
STORY_FIRST_TIME_SWAP(id = "first-time-swap-v2", analyticType = "Swap"), STORY_FIRST_TIME_SWAP(id = "first-time-swap-v2", analyticType = "Swap"),
STORY_FIRST_TIME_YIELD_PROMO(id = "first-time-yield-promo", analyticType = "YieldPromo"),
} }

View file

@ -17,6 +17,7 @@ dependencies {
implementation(projects.core.ui) implementation(projects.core.ui)
implementation(projects.core.utils) implementation(projects.core.utils)
implementation(projects.libs.blockchainSdk) implementation(projects.libs.blockchainSdk)
implementation(projects.libs.crypto)
/** Domain */ /** Domain */
implementation(projects.domain.account.status) implementation(projects.domain.account.status)

View file

@ -11,5 +11,6 @@ dependencies {
// region Other libraries // region Other libraries
implementation(deps.kotlin.serialization) implementation(deps.kotlin.serialization)
api(deps.kotlin.datetime)
} }

View file

@ -0,0 +1,24 @@
package com.tangem.domain.yield.supply.models
import kotlinx.datetime.Instant
sealed interface YieldBoostPromo {
data object None : YieldBoostPromo
data class Active(
val tokens: List<PromoToken>,
val timeline: Timeline,
val link: String?,
) : YieldBoostPromo {
data class PromoToken(
val contractAddress: String,
val tokenSymbol: String,
val tokenName: String,
val networkId: String,
)
data class Timeline(val start: Instant, val end: Instant)
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.domain.yield.supply.models
import kotlinx.datetime.Instant
sealed interface YieldBoostStatus {
data object NotStarted : YieldBoostStatus
/** User entered boost, qualification period is still running. */
data class Active(
val tokenName: String,
val networkId: String,
val moduleAddress: String,
val userAddress: String,
val contractAddress: String,
val activationDate: Instant,
val qualificationEndDate: Instant,
) : YieldBoostStatus
/** Boost has finished (backend `completed`). */
data class Completed(
val tokenName: String,
val networkId: String,
val moduleAddress: String,
val userAddress: String,
val contractAddress: String,
val activationDate: Instant,
val qualificationEndDate: Instant,
) : YieldBoostStatus
data class Disqualified(val reason: Reason) : YieldBoostStatus {
enum class Reason { FROD, LESS_THAN_1_USD, CLOSED, UNKNOWN }
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.domain.yield.supply.promo
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldBoostPromo
import com.tangem.domain.yield.supply.models.YieldBoostStatus
/**
* Backend yield-boost promo plumbing.
*
* Implementations keep an in-memory cache keyed by [UserWalletId]. On a refresh failure the cached
* value is returned. With an empty cache the call throws use cases swallow that to "hide UI".
*/
interface YieldPromoRepository {
@Throws
suspend fun getYieldBoostPromo(userWalletId: UserWalletId, forceRefresh: Boolean = false): YieldBoostPromo
@Throws
suspend fun getYieldBoostStatus(userWalletId: UserWalletId, forceRefresh: Boolean = false): YieldBoostStatus
}

View file

@ -0,0 +1,16 @@
package com.tangem.domain.yield.supply.promo.usecase
import java.math.BigDecimal
/**
* Pure boosted APY calculation. Hard-coded x3 coefficient single place to swap when the backend
* starts returning the coefficient explicitly.
*/
class GetBoostedApyUseCase {
operator fun invoke(baseApy: BigDecimal): BigDecimal = baseApy.multiply(BOOST_MULTIPLIER)
private companion object {
val BOOST_MULTIPLIER: BigDecimal = BigDecimal(3)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.yield.supply.promo.usecase
import arrow.core.Either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldBoostStatus
import com.tangem.domain.yield.supply.promo.YieldPromoRepository
class GetYieldBoostStatusUseCase(
private val repository: YieldPromoRepository,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
forceRefresh: Boolean = false,
): Either<Throwable, YieldBoostStatus> = Either.catch {
repository.getYieldBoostStatus(userWalletId, forceRefresh)
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.domain.yield.supply.promo.usecase
import arrow.core.Either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldBoostPromo
import com.tangem.domain.yield.supply.models.YieldBoostStatus
import com.tangem.domain.yield.supply.promo.YieldPromoRepository
import com.tangem.lib.crypto.BlockchainUtils
/**
* Returns `true` iff the given token is in the active promo list AND the user has not started boost yet.
*
* Short-circuits to `false` on:
* - non-Token currency
* - promo `None` (no active promo)
* - status not `NotStarted` (already Active / Completed / Disqualified)
*
* Any underlying repository failure surfaces as `Either.Left`.
*
* Feature-toggle and redesign-flag gating is the caller's responsibility keep this use case
* decoupled from feature-layer toggles to avoid the cyclic dependency `domain -> features`.
*/
class IsYieldBoostPromoEnabledForTokenUseCase(
private val repository: YieldPromoRepository,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Either<Throwable, Boolean> = Either.catch {
val token = cryptoCurrency as? CryptoCurrency.Token ?: return@catch false
val promo = repository.getYieldBoostPromo(userWalletId)
if (promo !is YieldBoostPromo.Active) return@catch false
val shouldIgnoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId)
val isTokenMatched = promo.tokens.any { promoToken ->
promoToken.contractAddress.equals(token.contractAddress, ignoreCase = shouldIgnoreCase) &&
promoToken.networkId == token.network.rawId
}
if (!isTokenMatched) return@catch false
val status = repository.getYieldBoostStatus(userWalletId)
status is YieldBoostStatus.NotStarted
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.domain.yield.supply.promo.usecase
import arrow.core.Either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldBoostPromo
import com.tangem.domain.yield.supply.models.YieldBoostStatus
import com.tangem.domain.yield.supply.promo.YieldPromoRepository
/**
* Returns `true` iff the main wallet boost banner should be shown:
* - promo is `Active` server-side
* - status is `NotStarted`
*
* Token ownership is intentionally NOT checked the banner is shown to every eligible wallet
* regardless of whether it currently holds a promo token.
*
* Any repository failure surfaces as `Either.Left` never assume eligibility on uncertainty.
* Feature-toggle / redesign / "user dismissed" gating is the caller's responsibility.
*/
class ShouldShowYieldBoostMainBannerUseCase(
private val repository: YieldPromoRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<Throwable, Boolean> = Either.catch {
val promo = repository.getYieldBoostPromo(userWalletId)
if (promo !is YieldBoostPromo.Active) return@catch false
val status = repository.getYieldBoostStatus(userWalletId)
status is YieldBoostStatus.NotStarted
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.domain.yield.supply.promo.usecase
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
import java.math.BigDecimal
class GetBoostedApyUseCaseTest {
private val useCase = GetBoostedApyUseCase()
@Test
fun `GIVEN base apy 5_1 WHEN invoke THEN returns 15_3`() {
val result = useCase(BigDecimal("5.1"))
assertThat(result).isEqualTo(BigDecimal("15.3"))
}
@Test
fun `GIVEN base apy 0 WHEN invoke THEN returns 0`() {
val result = useCase(BigDecimal.ZERO)
assertThat(result).isEqualTo(BigDecimal.ZERO.multiply(BigDecimal(3)))
}
@Test
fun `GIVEN base apy 4_99 WHEN invoke THEN returns 14_97`() {
val result = useCase(BigDecimal("4.99"))
assertThat(result).isEqualTo(BigDecimal("14.97"))
}
@Test
fun `GIVEN base apy 100 WHEN invoke THEN returns 300`() {
val result = useCase(BigDecimal("100"))
assertThat(result).isEqualTo(BigDecimal("300"))
}
}

View file

@ -0,0 +1,246 @@
package com.tangem.domain.yield.supply.promo.usecase
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldBoostPromo
import com.tangem.domain.yield.supply.models.YieldBoostStatus
import com.tangem.domain.yield.supply.promo.YieldPromoRepository
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
class IsYieldBoostPromoEnabledForTokenUseCaseTest {
private val repository: YieldPromoRepository = mockk()
private lateinit var useCase: IsYieldBoostPromoEnabledForTokenUseCase
private val userWalletId = UserWalletId("abcdef012345")
private val contractAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
private val networkRawId = "ethereum"
@BeforeEach
fun setUp() {
useCase = IsYieldBoostPromoEnabledForTokenUseCase(repository = repository)
}
@Test
fun `GIVEN currency is coin WHEN invoke THEN returns Right(false)`() = runTest {
val coin = createCoin()
val result = useCase(userWalletId, coin)
assertThat(result.getOrNull()).isFalse()
}
@Test
fun `GIVEN promo is None WHEN invoke THEN returns Right(false)`() = runTest {
val token = createToken()
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns YieldBoostPromo.None
val result = useCase(userWalletId, token)
assertThat(result.getOrNull()).isFalse()
}
@Test
fun `GIVEN promo repository throws WHEN invoke THEN returns Left`() = runTest {
val token = createToken()
coEvery { repository.getYieldBoostPromo(userWalletId, false) } throws RuntimeException("net")
val result = useCase(userWalletId, token)
assertThat(result.isLeft()).isTrue()
}
@Test
fun `GIVEN token not in promo list WHEN invoke THEN returns Right(false)`() = runTest {
val token = createToken(contractAddress = "0xdifferent")
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
val result = useCase(userWalletId, token)
assertThat(result.getOrNull()).isFalse()
}
@Test
fun `GIVEN network mismatch WHEN invoke THEN returns Right(false)`() = runTest {
val token = createToken(networkRawId = "polygon")
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
val result = useCase(userWalletId, token)
assertThat(result.getOrNull()).isFalse()
}
@Test
fun `GIVEN status repository throws WHEN invoke THEN returns Left`() = runTest {
val token = createToken()
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
coEvery { repository.getYieldBoostStatus(userWalletId, false) } throws RuntimeException("net")
val result = useCase(userWalletId, token)
assertThat(result.isLeft()).isTrue()
}
@Test
fun `GIVEN status is Active WHEN invoke THEN returns Right(false)`() = runTest {
val token = createToken()
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns activeStatus()
val result = useCase(userWalletId, token)
assertThat(result.getOrNull()).isFalse()
}
@Test
fun `GIVEN status is Completed WHEN invoke THEN returns Right(false)`() = runTest {
val token = createToken()
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns completedStatus()
val result = useCase(userWalletId, token)
assertThat(result.getOrNull()).isFalse()
}
@Test
fun `GIVEN status is Disqualified WHEN invoke THEN returns Right(false)`() = runTest {
val token = createToken()
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns
YieldBoostStatus.Disqualified(YieldBoostStatus.Disqualified.Reason.FROD)
val result = useCase(userWalletId, token)
assertThat(result.getOrNull()).isFalse()
}
@Test
fun `GIVEN status is NotStarted and token matches WHEN invoke THEN returns Right(true)`() = runTest {
val token = createToken()
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns YieldBoostStatus.NotStarted
val result = useCase(userWalletId, token)
assertThat(result.getOrNull()).isTrue()
}
@Test
fun `GIVEN contract address differs only in case on EVM WHEN invoke THEN returns Right(true)`() = runTest {
val token = createToken(contractAddress = contractAddress.uppercase())
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns YieldBoostStatus.NotStarted
val result = useCase(userWalletId, token)
assertThat(result.getOrNull()).isTrue()
}
private fun activePromo() = YieldBoostPromo.Active(
tokens = listOf(
YieldBoostPromo.Active.PromoToken(
contractAddress = contractAddress,
tokenSymbol = "USDC",
tokenName = "USD Coin",
networkId = networkRawId,
),
),
timeline = YieldBoostPromo.Active.Timeline(
start = Instant.parse("2026-01-01T00:00:00Z"),
end = Instant.parse("2027-01-01T00:00:00Z"),
),
link = null,
)
private fun activeStatus() = YieldBoostStatus.Active(
tokenName = "USD Coin",
networkId = networkRawId,
moduleAddress = "0xmodule",
userAddress = "0xuser",
contractAddress = contractAddress,
activationDate = Instant.parse("2026-05-01T00:00:00Z"),
qualificationEndDate = Instant.parse("2026-06-01T00:00:00Z"),
)
private fun completedStatus() = YieldBoostStatus.Completed(
tokenName = "USD Coin",
networkId = networkRawId,
moduleAddress = "0xmodule",
userAddress = "0xuser",
contractAddress = contractAddress,
activationDate = Instant.parse("2026-04-01T00:00:00Z"),
qualificationEndDate = Instant.parse("2026-05-01T00:00:00Z"),
)
private fun createToken(
contractAddress: String = this.contractAddress,
networkRawId: String = this.networkRawId,
): CryptoCurrency.Token {
val derivationPath = Network.DerivationPath.None
val network = Network(
id = Network.ID(value = networkRawId, derivationPath = derivationPath),
name = networkRawId,
currencySymbol = networkRawId.take(3).uppercase(),
derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.Unspecified("UNSPECIFIED"),
hasFiatFeeRate = true,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
)
return CryptoCurrency.Token(
id = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(networkRawId),
suffix = CryptoCurrency.ID.Suffix.RawID(networkRawId),
),
network = network,
name = "USDC",
symbol = "USDC",
decimals = 6,
iconUrl = null,
isCustom = false,
contractAddress = contractAddress,
)
}
private fun createCoin(): CryptoCurrency.Coin {
val derivationPath = Network.DerivationPath.None
val network = Network(
id = Network.ID(value = networkRawId, derivationPath = derivationPath),
name = networkRawId,
currencySymbol = "ETH",
derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.Unspecified("UNSPECIFIED"),
hasFiatFeeRate = true,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
)
return CryptoCurrency.Coin(
id = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(networkRawId),
suffix = CryptoCurrency.ID.Suffix.RawID(networkRawId),
),
network = network,
name = "Ethereum",
symbol = "ETH",
decimals = 18,
iconUrl = null,
isCustom = false,
)
}
}

View file

@ -0,0 +1,104 @@
package com.tangem.domain.yield.supply.promo.usecase
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.models.YieldBoostPromo
import com.tangem.domain.yield.supply.models.YieldBoostStatus
import com.tangem.domain.yield.supply.promo.YieldPromoRepository
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
class ShouldShowYieldBoostMainBannerUseCaseTest {
private val repository: YieldPromoRepository = mockk()
private lateinit var useCase: ShouldShowYieldBoostMainBannerUseCase
private val userWalletId = UserWalletId("abcdef012345")
private val contractAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
private val networkRawId = "ethereum"
@BeforeEach
fun setUp() {
useCase = ShouldShowYieldBoostMainBannerUseCase(repository = repository)
}
@Test
fun `GIVEN promo repository throws WHEN invoke THEN returns Left`() = runTest {
coEvery { repository.getYieldBoostPromo(userWalletId, false) } throws RuntimeException("net")
val result = useCase(userWalletId)
assertThat(result.isLeft()).isTrue()
}
@Test
fun `GIVEN promo is None WHEN invoke THEN returns Right(false)`() = runTest {
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns YieldBoostPromo.None
val result = useCase(userWalletId)
assertThat(result.getOrNull()).isFalse()
}
@Test
fun `GIVEN status repository throws WHEN invoke THEN returns Left`() = runTest {
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
coEvery { repository.getYieldBoostStatus(userWalletId, false) } throws RuntimeException("net")
val result = useCase(userWalletId)
assertThat(result.isLeft()).isTrue()
}
@Test
fun `GIVEN status is Active WHEN invoke THEN returns Right(false)`() = runTest {
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns activeStatus()
val result = useCase(userWalletId)
assertThat(result.getOrNull()).isFalse()
}
@Test
fun `GIVEN promo Active and status NotStarted WHEN invoke THEN returns Right(true)`() = runTest {
coEvery { repository.getYieldBoostPromo(userWalletId, false) } returns activePromo()
coEvery { repository.getYieldBoostStatus(userWalletId, false) } returns YieldBoostStatus.NotStarted
val result = useCase(userWalletId)
assertThat(result.getOrNull()).isTrue()
}
private fun activePromo() = YieldBoostPromo.Active(
tokens = listOf(
YieldBoostPromo.Active.PromoToken(
contractAddress = contractAddress,
tokenSymbol = "USDC",
tokenName = "USD Coin",
networkId = networkRawId,
),
),
timeline = YieldBoostPromo.Active.Timeline(
start = Instant.parse("2026-01-01T00:00:00Z"),
end = Instant.parse("2027-01-01T00:00:00Z"),
),
link = null,
)
private fun activeStatus() = YieldBoostStatus.Active(
tokenName = "USD Coin",
networkId = networkRawId,
moduleAddress = "0xmodule",
userAddress = "0xuser",
contractAddress = contractAddress,
activationDate = Instant.parse("2026-05-01T00:00:00Z"),
qualificationEndDate = Instant.parse("2026-06-01T00:00:00Z"),
)
}

View file

@ -10,6 +10,7 @@ interface StoriesComponent : ComposableContentComponent {
val storyId: String, val storyId: String,
val nextScreen: AppRoute? = null, val nextScreen: AppRoute? = null,
val screenSource: String, val screenSource: String,
val shouldMarkAsSeenOnClose: Boolean = true,
) )
interface Factory : ComponentFactory<Params, StoriesComponent> interface Factory : ComponentFactory<Params, StoriesComponent>

View file

@ -1,6 +1,7 @@
package com.tangem.feature.stories.impl package com.tangem.feature.stories.impl
import com.tangem.domain.stories.models.StoryContentIds import com.tangem.domain.stories.models.StoryContentIds
import com.tangem.core.res.R as CoreResR
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -13,6 +14,7 @@ internal object StoriesSlideConfigs {
fun getSlides(storyId: String): ImmutableList<SlideConfig> = when (storyId) { fun getSlides(storyId: String): ImmutableList<SlideConfig> = when (storyId) {
StoryContentIds.STORY_FIRST_TIME_SWAP.id -> swapSlides() StoryContentIds.STORY_FIRST_TIME_SWAP.id -> swapSlides()
StoryContentIds.STORY_FIRST_TIME_YIELD_PROMO.id -> yieldPromoSlides()
else -> persistentListOf() else -> persistentListOf()
} }
@ -34,4 +36,23 @@ internal object StoriesSlideConfigs {
com.tangem.core.res.R.string.swap_story_forth_subtitle_v2, com.tangem.core.res.R.string.swap_story_forth_subtitle_v2,
), ),
) )
private fun yieldPromoSlides(): ImmutableList<SlideConfig> = persistentListOf(
SlideConfig(
CoreResR.string.yield_apy_boost_story_first_title,
CoreResR.string.yield_apy_boost_story_first_subtitle,
),
SlideConfig(
CoreResR.string.yield_apy_boost_story_second_title,
CoreResR.string.yield_apy_boost_story_second_subtitle,
),
SlideConfig(
CoreResR.string.yield_apy_boost_story_third_title,
CoreResR.string.yield_apy_boost_story_third_subtitle,
),
SlideConfig(
CoreResR.string.yield_apy_boost_story_fourth_title,
CoreResR.string.yield_apy_boost_story_fourth_subtitle,
),
)
} }

View file

@ -40,7 +40,7 @@ internal class StoriesModel @Inject constructor(
private fun openScreen(hideStories: Boolean = true) { private fun openScreen(hideStories: Boolean = true) {
modelScope.launch { modelScope.launch {
if (hideStories) { if (hideStories && params.shouldMarkAsSeenOnClose) {
shouldShowStoriesUseCase.neverToShow(params.storyId) shouldShowStoriesUseCase.neverToShow(params.storyId)
} }
router.pop() router.pop()

View file

@ -33,8 +33,10 @@ import com.tangem.domain.settings.NeverToSuggestRateAppUseCase
import com.tangem.domain.settings.RemindToRateAppLaterUseCase import com.tangem.domain.settings.RemindToRateAppLaterUseCase
import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.stories.models.StoryContentIds
import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.wallets.usecase.* import com.tangem.domain.wallets.usecase.*
import com.tangem.domain.yield.supply.usecase.YieldSupplySetShouldShowMainPromoUseCase
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
@ -87,6 +89,10 @@ internal interface WalletWarningsClickIntents {
fun onDismissAssetsDiscoveryNotification(userWalletId: UserWalletId) fun onDismissAssetsDiscoveryNotification(userWalletId: UserWalletId)
fun onAssetsDiscoveryManageClick(userWalletId: UserWalletId) fun onAssetsDiscoveryManageClick(userWalletId: UserWalletId)
fun onYieldBoostBannerClick(userWalletId: UserWalletId)
fun onDismissYieldBoostBanner(userWalletId: UserWalletId)
} }
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@ -118,6 +124,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val reviewManager: ReviewManager, private val reviewManager: ReviewManager,
private val closeHotWalletUpgradeBannerUseCase: CloseHotWalletUpgradeBannerUseCase, private val closeHotWalletUpgradeBannerUseCase: CloseHotWalletUpgradeBannerUseCase,
private val acknowledgeAssetsDiscoveryCompletionUseCase: AcknowledgeAssetsDiscoveryCompletionUseCase, private val acknowledgeAssetsDiscoveryCompletionUseCase: AcknowledgeAssetsDiscoveryCompletionUseCase,
private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase,
) : BaseWalletClickIntents(), WalletWarningsClickIntents { ) : BaseWalletClickIntents(), WalletWarningsClickIntents {
override fun onAddBackupCardClick() { override fun onAddBackupCardClick() {
@ -408,4 +415,21 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
AccountId.forMainCryptoPortfolio(userWalletId), AccountId.forMainCryptoPortfolio(userWalletId),
) )
} }
override fun onYieldBoostBannerClick(userWalletId: UserWalletId) {
appRouter.push(
Stories(
storyId = StoryContentIds.STORY_FIRST_TIME_YIELD_PROMO.id,
nextScreen = null,
screenSource = "YieldMainBanner",
shouldMarkAsSeenOnClose = false,
),
)
}
override fun onDismissYieldBoostBanner(userWalletId: UserWalletId) {
modelScope.launch {
yieldSupplySetShouldShowMainPromoUseCase(shouldShow = false)
}
}
} }

View file

@ -98,6 +98,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.Warning.TangemPayRefreshNeeded -> null is WalletNotification.Warning.TangemPayRefreshNeeded -> null
is WalletNotification.Warning.TangemPayUnreachable -> null is WalletNotification.Warning.TangemPayUnreachable -> null
is WalletNotification.UpgradeHotWalletPromo -> null is WalletNotification.UpgradeHotWalletPromo -> null
is WalletNotification.YieldBoostPromo -> null
is WalletNotification.AssetsDiscoveryCompleted -> null is WalletNotification.AssetsDiscoveryCompleted -> null
is WalletNotification.CreateTangemPayAccount -> TangemPayAnalyticsEvents.PermanentBannerShowed() is WalletNotification.CreateTangemPayAccount -> TangemPayAnalyticsEvents.PermanentBannerShowed()
} }

View file

@ -5,6 +5,7 @@ package com.tangem.feature.wallet.presentation.wallet.domain
import com.tangem.common.ui.notifications.NotificationId import com.tangem.common.ui.notifications.NotificationId
import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.models.AccountStatusList
@ -28,7 +29,10 @@ import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.domain.yield.supply.promo.usecase.ShouldShowYieldBoostMainBannerUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.account.AccountDependencies
@ -60,6 +64,10 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase, private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase,
private val observeAssetsDiscoveryUseCase: ObserveAssetsDiscoveryUseCase, private val observeAssetsDiscoveryUseCase: ObserveAssetsDiscoveryUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val shouldShowYieldBoostMainBannerUseCase: ShouldShowYieldBoostMainBannerUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val designFeatureToggles: DesignFeatureToggles,
) { ) {
@Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType") @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType")
@ -87,6 +95,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
getUpgradeBannerClosureTimestampUseCase(userWallet.walletId) getUpgradeBannerClosureTimestampUseCase(userWallet.walletId)
.distinctUntilChanged(), .distinctUntilChanged(),
assetsDiscoveryProgressFlow, assetsDiscoveryProgressFlow,
yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged(),
) { array -> array } ) { array -> array }
.map { array -> .map { array ->
val accountStatusList = array[0] as AccountStatusList val accountStatusList = array[0] as AccountStatusList
@ -97,6 +106,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val shouldShowUpgradeBanner = array[5] as Boolean val shouldShowUpgradeBanner = array[5] as Boolean
val closureTimestamp = array[6] as? Long val closureTimestamp = array[6] as? Long
val assetsDiscoveryProgress = array[7] as AssetsDiscoveryProgress val assetsDiscoveryProgress = array[7] as AssetsDiscoveryProgress
val shouldShowYieldBoostPromoLocal = array[8] as Boolean
val flattenCurrencies = accountStatusList.flattenCurrencies() val flattenCurrencies = accountStatusList.flattenCurrencies()
val paymentAccountStatus = accountStatusList.accountStatuses val paymentAccountStatus = accountStatusList.accountStatuses
@ -165,10 +175,34 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
walletClickIntents = clickIntents, walletClickIntents = clickIntents,
) )
} }
addYieldBoostBannerNotification(
userWallet = userWallet,
shouldShowLocal = shouldShowYieldBoostPromoLocal,
clickIntents = clickIntents,
)
}.toImmutableList() }.toImmutableList()
} }
} }
private suspend fun MutableList<WalletNotification>.addYieldBoostBannerNotification(
userWallet: UserWallet,
shouldShowLocal: Boolean,
clickIntents: WalletClickIntents,
) {
if (!shouldShowLocal) return
if (!yieldSupplyFeatureToggles.isYieldPromoEnabled) return
if (designFeatureToggles.isRedesignEnabled) return
val shouldShow = shouldShowYieldBoostMainBannerUseCase(userWallet.walletId).getOrNull() == true
if (!shouldShow) return
add(
WalletNotification.YieldBoostPromo(
onClick = { clickIntents.onYieldBoostBannerClick(userWallet.walletId) },
onCloseClick = { clickIntents.onDismissYieldBoostBanner(userWallet.walletId) },
),
)
}
private fun MutableList<WalletNotification>.addTangemPayWarnings( private fun MutableList<WalletNotification>.addTangemPayWarnings(
status: AccountStatus.Payment, status: AccountStatus.Payment,
userWallet: UserWallet, userWallet: UserWallet,

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.extensions.wrappedList
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.impl.R
@ -319,6 +320,27 @@ sealed class WalletNotification(val config: NotificationConfig) {
), ),
) )
data class YieldBoostPromo(
val onClick: () -> Unit,
val onCloseClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = com.tangem.core.ui.extensions.combinedReference(
resourceReference(com.tangem.core.res.R.string.yield_apy_boost_banner_title),
stringReference(" · "),
resourceReference(com.tangem.core.res.R.string.yield_apy_boost_banner_title_apy_multiplied),
),
subtitle = resourceReference(com.tangem.core.res.R.string.yield_apy_boost_banner_subtitle),
iconResId = com.tangem.core.ui.R.drawable.ic_analytics_up_24,
iconTint = IconTint.Accent,
onCloseClick = onCloseClick,
buttonsState = ButtonsState.PrimaryButtonConfig(
text = resourceReference(com.tangem.core.res.R.string.yield_apy_boost_banner_button_title),
onClick = onClick,
),
),
)
data class AssetsDiscoveryCompleted( data class AssetsDiscoveryCompleted(
val onCloseClick: () -> Unit, val onCloseClick: () -> Unit,
val onManageTokensClick: () -> Unit, val onManageTokensClick: () -> Unit,

View file

@ -1,6 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.models.wallet.UserWallet 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.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender 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.analytics.utils.WalletWarningsSingleEventSender
@ -15,7 +17,9 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]")
internal class MultiWalletWarningsSubscriber @AssistedInject constructor( internal class MultiWalletWarningsSubscriber @AssistedInject constructor(
@Assisted private val userWallet: UserWallet, @Assisted private val userWallet: UserWallet,
@ -24,6 +28,7 @@ internal class MultiWalletWarningsSubscriber @AssistedInject constructor(
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
private val getStoryContentUseCase: GetStoryContentUseCase,
) : WalletSubscriber() { ) : WalletSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<ImmutableList<WalletNotification>> { override fun create(coroutineScope: CoroutineScope): Flow<ImmutableList<WalletNotification>> {
@ -31,6 +36,15 @@ internal class MultiWalletWarningsSubscriber @AssistedInject constructor(
.conflate() .conflate()
.distinctUntilChanged() .distinctUntilChanged()
.onEach { warnings -> .onEach { warnings ->
if (warnings.any { it is WalletNotification.YieldBoostPromo }) {
coroutineScope.launch {
getStoryContentUseCase.invokeSync(
id = StoryContentIds.STORY_FIRST_TIME_YIELD_PROMO.id,
refresh = true,
)
}
}
val displayedState = stateController.getWalletState(userWallet.walletId) val displayedState = stateController.getWalletState(userWallet.walletId)
// Wait until the wallet appears in the list // Wait until the wallet appears in the list

View file

@ -2,11 +2,18 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import com.tangem.common.ui.notifications.CreatePaymentAccountNotification import com.tangem.common.ui.notifications.CreatePaymentAccountNotification
import com.tangem.core.ui.components.notifications.NoteMigrationNotification import com.tangem.core.ui.components.notifications.NoteMigrationNotification
import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.extensions.annotatedReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.ForceDarkTheme import com.tangem.core.ui.res.ForceDarkTheme
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.impl.R
@ -48,6 +55,14 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
) )
} }
} }
is WalletNotification.YieldBoostPromo -> {
Notification(
config = item.config.copy(title = annotatedReference(yieldBoostPromoTitle())),
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
iconTint = TangemTheme.colors.icon.accent,
subtitleColor = TangemTheme.colors.text.secondary,
)
}
is WalletNotification.CreateTangemPayAccount -> { is WalletNotification.CreateTangemPayAccount -> {
CreatePaymentAccountNotification( CreatePaymentAccountNotification(
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
@ -76,4 +91,18 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
} }
}, },
) )
}
@Composable
private fun yieldBoostPromoTitle(): AnnotatedString {
val accent = TangemTheme.colors.text.accent
val baseTitle = stringResourceSafe(com.tangem.core.res.R.string.yield_apy_boost_banner_title)
val tailTitle = stringResourceSafe(com.tangem.core.res.R.string.yield_apy_boost_banner_title_apy_multiplied)
return buildAnnotatedString {
append(baseTitle)
append(" · ")
withStyle(SpanStyle(color = accent)) {
append(tailTitle)
}
}
} }

View file

@ -0,0 +1,5 @@
package com.tangem.features.yield.supply.api
interface YieldSupplyFeatureToggles {
val isYieldPromoEnabled: Boolean
}

View file

@ -11,6 +11,7 @@ interface YieldSupplyPromoComponent : ComposableContentComponent {
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
val currency: CryptoCurrency, val currency: CryptoCurrency,
val apy: String, val apy: String,
val isPromoEnabled: Boolean = false,
) )
interface Factory : ComponentFactory<Params, YieldSupplyPromoComponent> interface Factory : ComponentFactory<Params, YieldSupplyPromoComponent>

View file

@ -15,6 +15,7 @@ sealed class YieldSupplyEntryRoute : Route {
data class Promo( data class Promo(
val cryptoCurrency: CryptoCurrency, val cryptoCurrency: CryptoCurrency,
val apy: String, val apy: String,
val isPromoEnabled: Boolean = false,
) : YieldSupplyEntryRoute() ) : YieldSupplyEntryRoute()
/** Route to yield supply active screen */ /** Route to yield supply active screen */

View file

@ -58,6 +58,8 @@ dependencies {
implementation(projects.domain.transaction) implementation(projects.domain.transaction)
implementation(projects.domain.yieldSupply.models) implementation(projects.domain.yieldSupply.models)
implementation(projects.domain.yieldSupply) implementation(projects.domain.yieldSupply)
implementation(projects.domain.stories.models)
implementation(projects.domain.stories)
implementation(projects.domain.feedback.models) implementation(projects.domain.feedback.models)
implementation(projects.domain.feedback) implementation(projects.domain.feedback)
implementation(projects.domain.balanceHiding.models) implementation(projects.domain.balanceHiding.models)
@ -76,6 +78,7 @@ dependencies {
implementation(deps.decompose) implementation(deps.decompose)
implementation(deps.decompose.ext.compose) implementation(deps.decompose.ext.compose)
implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.immutable.collections)
implementation(deps.kotlin.datetime)
/** DI */ /** DI */
implementation(deps.hilt.android) implementation(deps.hilt.android)

View file

@ -0,0 +1,15 @@
package com.tangem.features.yield.supply.impl
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import javax.inject.Inject
internal class DefaultYieldSupplyFeatureToggles @Inject constructor(
featureTogglesManager: FeatureTogglesManager,
) : YieldSupplyFeatureToggles {
override val isYieldPromoEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15154_YIELD_PROMO_ENABLED,
)
}

View file

@ -0,0 +1,28 @@
package com.tangem.features.yield.supply.impl
import com.tangem.core.ui.coil.ImagePreloader
import com.tangem.domain.stories.GetStoryContentUseCase
import com.tangem.domain.stories.models.StoryContentIds
import com.tangem.utils.coroutines.runSuspendCatching
import javax.inject.Inject
/**
* Warms the in-memory `StoriesStore` cache (and Coil image cache) for the yield-boost story.
*
* Called proactively from yield-supply models so that when the user taps "Learn more" /
* the active-boost row, [com.tangem.feature.stories.impl.model.StoriesModel] hits cache
* instead of waiting for the 1-second network fetch.
*/
internal class YieldBoostStoryPreloader @Inject constructor(
private val getStoryContentUseCase: GetStoryContentUseCase,
private val imagePreloader: ImagePreloader,
) {
suspend fun preload() {
runSuspendCatching {
getStoryContentUseCase
.invokeSync(id = StoryContentIds.STORY_FIRST_TIME_YIELD_PROMO.id, refresh = true)
.onRight { story -> story?.getImageUrls()?.forEach(imagePreloader::preload) }
}
}
}

View file

@ -17,4 +17,6 @@ internal data class YieldSupplyActiveContentUM(
val minFeeDescription: TextReference?, val minFeeDescription: TextReference?,
val apy: TextReference? = null, val apy: TextReference? = null,
val isHighFee: Boolean = false, val isHighFee: Boolean = false,
val boostText: TextReference? = null,
val onBoostClick: () -> Unit = {},
) )

View file

@ -11,7 +11,10 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.extensions.wrappedList
@ -25,15 +28,23 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.common.routing.AppRoute
import com.tangem.domain.stories.models.StoryContentIds
import com.tangem.domain.yield.supply.models.YieldBoostStatus
import com.tangem.domain.yield.supply.promo.usecase.GetYieldBoostStatusUseCase
import com.tangem.domain.yield.supply.usecase.* import com.tangem.domain.yield.supply.usecase.*
import com.tangem.features.yield.supply.api.YieldSupplyActiveComponent import com.tangem.features.yield.supply.api.YieldSupplyActiveComponent
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.R
import com.tangem.core.res.R as CoreResR
import com.tangem.features.yield.supply.impl.YieldBoostStoryPreloader
import com.tangem.features.yield.supply.impl.active.entity.YieldSupplyActiveContentUM import com.tangem.features.yield.supply.impl.active.entity.YieldSupplyActiveContentUM
import com.tangem.features.yield.supply.impl.active.model.transformers.YieldSupplyActiveFeeContentTransformer import com.tangem.features.yield.supply.impl.active.model.transformers.YieldSupplyActiveFeeContentTransformer
import com.tangem.features.yield.supply.impl.active.model.transformers.YieldSupplyActiveMinAmountTransformer import com.tangem.features.yield.supply.impl.active.model.transformers.YieldSupplyActiveMinAmountTransformer
import com.tangem.features.yield.supply.impl.subcomponents.approve.YieldSupplyApproveComponent import com.tangem.features.yield.supply.impl.subcomponents.approve.YieldSupplyApproveComponent
import com.tangem.features.yield.supply.impl.subcomponents.stopearning.YieldSupplyStopEarningComponent import com.tangem.features.yield.supply.impl.subcomponents.stopearning.YieldSupplyStopEarningComponent
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
@ -41,7 +52,10 @@ import com.tangem.utils.transformer.update
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds
@Suppress("LongParameterList", "LargeClass") @Suppress("LongParameterList", "LargeClass")
@ModelScoped @ModelScoped
@ -60,6 +74,10 @@ internal class YieldSupplyActiveModel @Inject constructor(
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
private val appRouter: AppRouter, private val appRouter: AppRouter,
private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase, private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase,
private val getYieldBoostStatusUseCase: GetYieldBoostStatusUseCase,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val designFeatureToggles: DesignFeatureToggles,
private val boostStoryPreloader: YieldBoostStoryPreloader,
) : Model(), YieldSupplyStopEarningComponent.ModelCallback, ) : Model(), YieldSupplyStopEarningComponent.ModelCallback,
YieldSupplyApproveComponent.ModelCallback { YieldSupplyApproveComponent.ModelCallback {
@ -112,6 +130,8 @@ internal class YieldSupplyActiveModel @Inject constructor(
), ),
) )
subscribeOnCurrencyStatusUpdates() subscribeOnCurrencyStatusUpdates()
loadBoostBlock()
modelScope.launch(dispatchers.io) { boostStoryPreloader.preload() }
modelScope.launch(dispatchers.default) { modelScope.launch(dispatchers.default) {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
@ -219,6 +239,72 @@ internal class YieldSupplyActiveModel @Inject constructor(
} }
} }
private fun loadBoostBlock() {
if (!yieldSupplyFeatureToggles.isYieldPromoEnabled) return
if (designFeatureToggles.isRedesignEnabled) return
modelScope.launch(dispatchers.io) {
val status = getYieldBoostStatusUseCase(userWalletId).getOrNull() ?: return@launch
val token = cryptoCurrency as? CryptoCurrency.Token ?: return@launch
when {
status is YieldBoostStatus.Active && status.matches(token) -> {
uiState.update {
it.copy(boostText = buildActiveBoostText(status), onBoostClick = ::onBoostClick)
}
}
status is YieldBoostStatus.Completed && status.matches(token) -> {
uiState.update {
it.copy(
boostText = resourceReference(CoreResR.string.yield_promo_completed),
onBoostClick = ::onBoostClick,
)
}
}
}
}
}
private fun onBoostClick() {
appRouter.push(
AppRoute.Stories(
storyId = StoryContentIds.STORY_FIRST_TIME_YIELD_PROMO.id,
nextScreen = null,
screenSource = "YieldActive",
shouldMarkAsSeenOnClose = false,
),
)
}
private fun buildActiveBoostText(status: YieldBoostStatus.Active): TextReference {
val daysLeft = computeDaysLeft(status.qualificationEndDate.toEpochMilliseconds())
return combinedReference(
pluralReference(
id = CoreResR.plurals.common_days,
count = daysLeft,
formatArgs = wrappedList(daysLeft),
),
stringReference(" "),
resourceReference(CoreResR.string.yield_promo_left_title),
)
}
private fun computeDaysLeft(qualificationEndEpochMillis: Long): Int {
val nowMillis = Clock.System.now().toEpochMilliseconds()
val deltaMillis = max(qualificationEndEpochMillis - nowMillis, 0L)
return deltaMillis.milliseconds.inWholeDays.toInt()
}
private fun YieldBoostStatus.Active.matches(token: CryptoCurrency.Token): Boolean =
matchesToken(contractAddress = contractAddress, networkId = networkId, token = token)
private fun YieldBoostStatus.Completed.matches(token: CryptoCurrency.Token): Boolean =
matchesToken(contractAddress = contractAddress, networkId = networkId, token = token)
private fun matchesToken(contractAddress: String, networkId: String, token: CryptoCurrency.Token): Boolean {
val shouldIgnoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId)
return contractAddress.equals(token.contractAddress, ignoreCase = shouldIgnoreCase) &&
networkId == token.network.rawId
}
private fun loadApy() { private fun loadApy() {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
modelScope.launch(dispatchers.default) { modelScope.launch(dispatchers.default) {

View file

@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -43,6 +44,7 @@ import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.active.entity.YieldSupplyActiveContentUM import com.tangem.features.yield.supply.impl.active.entity.YieldSupplyActiveContentUM
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@Suppress("LongMethod")
@Composable @Composable
internal fun YieldSupplyActiveContent( internal fun YieldSupplyActiveContent(
state: YieldSupplyActiveContentUM, state: YieldSupplyActiveContentUM,
@ -61,15 +63,29 @@ internal fun YieldSupplyActiveContent(
), ),
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.action) .background(TangemTheme.colors.background.action)
.fillMaxWidth() .fillMaxWidth(),
.padding(12.dp),
) { ) {
CurrentApy(state.apy) Column(
chartComponent.Content(Modifier) verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(12.dp),
) {
CurrentApy(state.apy)
chartComponent.Content(Modifier)
}
AnimatedVisibility(state.boostText != null) {
Column {
HorizontalDivider(
thickness = TangemTheme.dimens.size0_5,
color = TangemTheme.colors.stroke.primary,
)
state.boostText?.let { boostText ->
BoostRow(text = boostText, onClick = state.onBoostClick)
}
}
}
} }
AnimatedVisibility(state.notifications.isNotEmpty()) { AnimatedVisibility(state.notifications.isNotEmpty()) {
@ -359,6 +375,37 @@ private fun HighComissionInfoRow(title: TextReference, info: TextReference?, isH
} }
} }
@Composable
private fun BoostRow(text: TextReference, onClick: () -> Unit, modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 12.dp, vertical = 12.dp),
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_gift_promo_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
)
Text(
text = text.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.weight(1f)
.padding(start = 12.dp),
)
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier.size(20.dp),
)
}
}
// region Preview // region Preview
@Composable @Composable
@Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360)

View file

@ -0,0 +1,21 @@
package com.tangem.features.yield.supply.impl.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.features.yield.supply.impl.DefaultYieldSupplyFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object YieldSupplyFeatureModule {
@Provides
@Singleton
fun provideYieldSupplyFeatureToggles(featureTogglesManager: FeatureTogglesManager): YieldSupplyFeatureToggles {
return DefaultYieldSupplyFeatureToggles(featureTogglesManager)
}
}

View file

@ -86,6 +86,7 @@ internal class DefaultYieldSupplyEntryComponent @AssistedInject constructor(
userWalletId = params.userWalletId, userWalletId = params.userWalletId,
currency = configuration.cryptoCurrency, currency = configuration.cryptoCurrency,
apy = configuration.apy, apy = configuration.apy,
isPromoEnabled = configuration.isPromoEnabled,
), ),
) )
is YieldSupplyEntryRoute.Active -> yieldSupplyActiveComponentFactory.create( is YieldSupplyEntryRoute.Active -> yieldSupplyActiveComponentFactory.create(

View file

@ -1,17 +1,21 @@
package com.tangem.features.yield.supply.impl.entry.model package com.tangem.features.yield.supply.impl.entry.model
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.yield.supply.promo.usecase.IsYieldBoostPromoEnabledForTokenUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase
import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.features.yield.supply.api.entry.YieldSupplyEntryRoute import com.tangem.features.yield.supply.api.entry.YieldSupplyEntryRoute
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -20,12 +24,16 @@ import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject import javax.inject.Inject
@ModelScoped @ModelScoped
@Suppress("LongParameterList")
internal class YieldSupplyEntryModel @Inject constructor( internal class YieldSupplyEntryModel @Inject constructor(
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val router: Router, private val router: Router,
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase, private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val isYieldBoostPromoEnabledForTokenUseCase: IsYieldBoostPromoEnabledForTokenUseCase,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val designFeatureToggles: DesignFeatureToggles,
) : Model() { ) : Model() {
private val params = paramsContainer.require<YieldSupplyEntryComponent.Params>() private val params = paramsContainer.require<YieldSupplyEntryComponent.Params>()
@ -90,7 +98,14 @@ internal class YieldSupplyEntryModel @Inject constructor(
return if (isActiveYield) { return if (isActiveYield) {
YieldSupplyEntryRoute.Active(cryptoCurrency = token) YieldSupplyEntryRoute.Active(cryptoCurrency = token)
} else { } else {
YieldSupplyEntryRoute.Promo(cryptoCurrency = token, apy = params.apy) val isPromoEnabled = yieldSupplyFeatureToggles.isYieldPromoEnabled &&
!designFeatureToggles.isRedesignEnabled &&
isYieldBoostPromoEnabledForTokenUseCase(userWalletId, token).getOrElse { false }
YieldSupplyEntryRoute.Promo(
cryptoCurrency = token,
apy = params.apy,
isPromoEnabled = isPromoEnabled,
)
} }
} }
} }

View file

@ -13,6 +13,8 @@ internal sealed class YieldSupplyUM {
val apyText: TextReference, val apyText: TextReference,
val title: TextReference, val title: TextReference,
val onClick: () -> Unit, val onClick: () -> Unit,
val onLearnMoreClick: () -> Unit,
val isBoostAvailable: Boolean = false,
) : YieldSupplyUM() ) : YieldSupplyUM()
data object Loading : YieldSupplyUM() data object Loading : YieldSupplyUM()

View file

@ -3,4 +3,5 @@ package com.tangem.features.yield.supply.impl.main.model
interface YieldSupplyClickIntents { interface YieldSupplyClickIntents {
fun onStartEarningClick() fun onStartEarningClick()
fun onActiveClick() fun onActiveClick()
fun onLearnMoreClick()
} }

View file

@ -8,6 +8,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
@ -24,12 +25,17 @@ import com.tangem.domain.models.currency.shouldShowNotSuppliedNotification
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.yield.supply.YieldSupplyStatus import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.stories.models.StoryContentIds
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import com.tangem.domain.yield.supply.promo.usecase.GetBoostedApyUseCase
import com.tangem.domain.yield.supply.promo.usecase.IsYieldBoostPromoEnabledForTokenUseCase
import com.tangem.domain.yield.supply.usecase.* import com.tangem.domain.yield.supply.usecase.*
import com.tangem.features.yield.supply.api.YieldSupplyComponent import com.tangem.features.yield.supply.api.YieldSupplyComponent
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.YieldBoostStoryPreloader
import com.tangem.common.ui.earn.EarnBlockUM import com.tangem.common.ui.earn.EarnBlockUM
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.features.yield.supply.impl.main.model.converter.YieldSupplyToEarnBlockConverter import com.tangem.features.yield.supply.impl.main.model.converter.YieldSupplyToEarnBlockConverter
@ -61,6 +67,11 @@ internal class YieldSupplyModel @Inject constructor(
private val yieldSupplyEnterStatusFlowUseCase: YieldSupplyEnterStatusFlowUseCase, private val yieldSupplyEnterStatusFlowUseCase: YieldSupplyEnterStatusFlowUseCase,
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase, private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase, private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase,
private val isYieldBoostPromoEnabledForTokenUseCase: IsYieldBoostPromoEnabledForTokenUseCase,
private val getBoostedApyUseCase: GetBoostedApyUseCase,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
private val designFeatureToggles: DesignFeatureToggles,
private val boostStoryPreloader: YieldBoostStoryPreloader,
) : Model(), YieldSupplyClickIntents { ) : Model(), YieldSupplyClickIntents {
private val earnBlockConverter = YieldSupplyToEarnBlockConverter() private val earnBlockConverter = YieldSupplyToEarnBlockConverter()
@ -82,6 +93,7 @@ internal class YieldSupplyModel @Inject constructor(
init { init {
checkIfYieldSupplyIsAvailable() checkIfYieldSupplyIsAvailable()
modelScope.launch(dispatchers.io) { boostStoryPreloader.preload() }
} }
private fun checkIfYieldSupplyIsAvailable() { private fun checkIfYieldSupplyIsAvailable() {
@ -150,10 +162,17 @@ internal class YieldSupplyModel @Inject constructor(
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
.onRight { tokenStatus -> .onRight { tokenStatus ->
val isPromoEnabled = yieldSupplyFeatureToggles.isYieldPromoEnabled &&
!designFeatureToggles.isRedesignEnabled &&
isYieldBoostPromoEnabledForTokenUseCase(params.userWalletId, cryptoCurrencyToken)
.getOrElse { false }
val boostedApy = if (isPromoEnabled) getBoostedApyUseCase(tokenStatus.apy) else null
uiStateLegacy.update( uiStateLegacy.update(
YieldSupplyTokenStatusSuccessTransformer( YieldSupplyTokenStatusSuccessTransformer(
tokenStatus = tokenStatus, tokenStatus = tokenStatus,
onStartEarningClick = ::onStartEarningClick, onStartEarningClick = ::onStartEarningClick,
onLearnMoreClick = ::onLearnMoreClick,
boostedApy = boostedApy,
), ),
) )
}.onLeft { error -> }.onLeft { error ->
@ -170,19 +189,33 @@ internal class YieldSupplyModel @Inject constructor(
navigateToYieldSupplyEntry() navigateToYieldSupplyEntry()
} }
override fun onLearnMoreClick() {
appRouter.push(
AppRoute.Stories(
storyId = StoryContentIds.STORY_FIRST_TIME_YIELD_PROMO.id,
nextScreen = buildYieldEntryRoute(),
screenSource = "TokenDetails",
shouldMarkAsSeenOnClose = false,
),
)
}
private fun navigateToYieldSupplyEntry() { private fun navigateToYieldSupplyEntry() {
val cryptoCurrencyStatus = latestCryptoCurrencyStatus ?: return val route = buildYieldEntryRoute() ?: return
appRouter.push(route)
}
private fun buildYieldEntryRoute(): AppRoute.YieldSupplyEntry? {
val cryptoCurrencyStatus = latestCryptoCurrencyStatus ?: return null
val apy = when (val yieldSupplyUM = uiStateLegacy.value) { val apy = when (val yieldSupplyUM = uiStateLegacy.value) {
is YieldSupplyUM.Available -> yieldSupplyUM.apy is YieldSupplyUM.Available -> yieldSupplyUM.apy
is YieldSupplyUM.Content -> yieldSupplyUM.apy is YieldSupplyUM.Content -> yieldSupplyUM.apy
else -> "" else -> ""
} }
appRouter.push( return AppRoute.YieldSupplyEntry(
AppRoute.YieldSupplyEntry( userWalletId = params.userWalletId,
userWalletId = params.userWalletId, cryptoCurrency = cryptoCurrencyStatus.currency,
cryptoCurrency = cryptoCurrencyStatus.currency, apy = apy,
apy = apy,
),
) )
} }

View file

@ -1,5 +1,10 @@
package com.tangem.features.yield.supply.impl.main.model.transformers package com.tangem.features.yield.supply.impl.main.model.transformers
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import com.tangem.core.ui.extensions.annotatedReference
import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
@ -7,27 +12,45 @@ import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.utils.transformer.Transformer import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal
internal class YieldSupplyTokenStatusSuccessTransformer( internal class YieldSupplyTokenStatusSuccessTransformer(
private val tokenStatus: YieldMarketToken, private val tokenStatus: YieldMarketToken,
private val onStartEarningClick: () -> Unit, private val onStartEarningClick: () -> Unit,
private val onLearnMoreClick: () -> Unit,
private val boostedApy: BigDecimal? = null,
) : Transformer<YieldSupplyUM> { ) : Transformer<YieldSupplyUM> {
override fun transform(prevState: YieldSupplyUM): YieldSupplyUM { override fun transform(prevState: YieldSupplyUM): YieldSupplyUM {
if (!tokenStatus.isActive) return YieldSupplyUM.Unavailable if (!tokenStatus.isActive) return YieldSupplyUM.Unavailable
val boost = boostedApy
return YieldSupplyUM.Available( return YieldSupplyUM.Available(
title = resourceReference( title = if (boost != null) {
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, resourceReference(R.string.yield_apy_boost_banner_title)
), } else {
resourceReference(R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title)
},
onClick = onStartEarningClick, onClick = onStartEarningClick,
onLearnMoreClick = onLearnMoreClick,
isBoostAvailable = boost != null,
apy = tokenStatus.apy.toString(), apy = tokenStatus.apy.toString(),
apyText = combinedReference( apyText = if (boost != null) {
resourceReference( annotatedReference(buildBoostedApyText(baseApy = tokenStatus.apy, boostedApy = boost))
R.string.yield_module_token_details_earn_notification_apy, } else {
), combinedReference(
stringReference(" ${tokenStatus.apy}%"), resourceReference(R.string.yield_module_token_details_earn_notification_apy),
), stringReference(" ${tokenStatus.apy}%"),
)
},
) )
} }
private fun buildBoostedApyText(baseApy: BigDecimal, boostedApy: BigDecimal) = buildAnnotatedString {
append("APY ")
withStyle(SpanStyle(textDecoration = TextDecoration.LineThrough)) {
append("$baseApy%")
}
append(" x3 → $boostedApy%")
}
} }

View file

@ -25,6 +25,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.SpacerW8
@ -64,21 +65,91 @@ internal fun YieldSupplyBlockContentLegacy(yieldSupplyUM: YieldSupplyUM, modifie
@Composable @Composable
private fun SupplyAvailable(supplyUM: YieldSupplyUM.Available, modifier: Modifier = Modifier) { private fun SupplyAvailable(supplyUM: YieldSupplyUM.Available, modifier: Modifier = Modifier) {
SupplyInfo( if (supplyUM.isBoostAvailable) {
title = resourceReference(R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title), SupplyAvailableBoosted(supplyUM = supplyUM, modifier = modifier)
subtitle = resourceReference(R.string.yield_module_token_details_earn_notification_description), } else {
rewardsApy = supplyUM.apyText, SupplyInfo(
iconTint = TangemTheme.colors.icon.accent, title = resourceReference(
modifier = modifier, R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
button = { ),
subtitle = resourceReference(R.string.yield_module_token_details_earn_notification_description),
rewardsApy = supplyUM.apyText,
iconTint = TangemTheme.colors.icon.accent,
modifier = modifier,
button = {
SecondaryButton(
text = stringResourceSafe(R.string.common_learn_more),
onClick = supplyUM.onClick,
size = TangemButtonSize.WideAction,
modifier = Modifier.fillMaxWidth(),
)
},
)
}
}
@Composable
private fun SupplyAvailableBoosted(supplyUM: YieldSupplyUM.Available, modifier: Modifier = Modifier) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.primary)
.padding(12.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.Top,
modifier = Modifier.fillMaxWidth(),
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_analytics_up_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
modifier = Modifier
.background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f), CircleShape)
.padding(6.dp)
.size(24.dp),
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.weight(1f),
) {
Text(
text = supplyUM.title.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
text = supplyUM.apyText.resolveAnnotatedReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.accent,
)
Text(
text = stringResourceSafe(R.string.yield_apy_boost_banner_subtitle),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(),
) {
SecondaryButton( SecondaryButton(
text = stringResourceSafe(R.string.common_learn_more), text = stringResourceSafe(R.string.common_learn_more),
onClick = supplyUM.onLearnMoreClick,
size = TangemButtonSize.WideAction,
modifier = Modifier.weight(1f),
)
PrimaryButton(
text = stringResourceSafe(R.string.common_activate),
onClick = supplyUM.onClick, onClick = supplyUM.onClick,
size = TangemButtonSize.WideAction, size = TangemButtonSize.WideAction,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.weight(1f),
) )
}, }
) }
} }
@Suppress("LongMethod") @Suppress("LongMethod")
@ -345,6 +416,15 @@ private class PreviewProvider : PreviewParameterProvider<YieldSupplyUM> {
apy = "5.1", apy = "5.1",
apyText = stringReference("5.1 % APY"), apyText = stringReference("5.1 % APY"),
onClick = {}, onClick = {},
onLearnMoreClick = {},
),
YieldSupplyUM.Available(
title = TextReference.Res(R.string.yield_apy_boost_banner_title),
apy = "5.1",
apyText = stringReference("APY 5.1% x3 → 15.3%"),
onClick = {},
onLearnMoreClick = {},
isBoostAvailable = true,
), ),
YieldSupplyUM.Content( YieldSupplyUM.Content(
title = stringReference("Aave l"), title = stringReference("Aave l"),

View file

@ -5,7 +5,11 @@ import com.tangem.core.ui.extensions.TextReference
data class YieldSupplyPromoUM( data class YieldSupplyPromoUM(
val tosLink: String, val tosLink: String,
val policyLink: String, val policyLink: String,
val boostTermsLink: String,
val title: TextReference, val title: TextReference,
val subtitle: TextReference, val subtitle: TextReference,
val tokenSymbol: String, val tokenSymbol: String,
val isBoostAvailable: Boolean = false,
val baseApy: String? = null,
val boostedApy: String? = null,
) )

View file

@ -3,6 +3,7 @@ package com.tangem.features.yield.supply.impl.promo.model
import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.TangemBlogUrlBuilder import com.tangem.common.TangemBlogUrlBuilder
import com.tangem.common.TangemSiteUrlBuilder
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
@ -11,15 +12,19 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.yield.supply.promo.usecase.GetBoostedApyUseCase
import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.YieldBoostStoryPreloader
import com.tangem.features.yield.supply.impl.promo.YieldSupplyPromoConfig import com.tangem.features.yield.supply.impl.promo.YieldSupplyPromoConfig
import com.tangem.features.yield.supply.impl.promo.entity.YieldSupplyPromoUM import com.tangem.features.yield.supply.impl.promo.entity.YieldSupplyPromoUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.math.BigDecimal
import javax.inject.Inject import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped @ModelScoped
internal class YieldSupplyPromoModel @Inject constructor( internal class YieldSupplyPromoModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
@ -27,22 +32,15 @@ internal class YieldSupplyPromoModel @Inject constructor(
private val analytics: AnalyticsEventHandler, private val analytics: AnalyticsEventHandler,
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
private val appRouter: AppRouter, private val appRouter: AppRouter,
private val getBoostedApyUseCase: GetBoostedApyUseCase,
private val boostStoryPreloader: YieldBoostStoryPreloader,
) : Model(), YieldSupplyPromoClickIntents { ) : Model(), YieldSupplyPromoClickIntents {
val params: YieldSupplyPromoComponent.Params = paramsContainer.require() val params: YieldSupplyPromoComponent.Params = paramsContainer.require()
val uiState: YieldSupplyPromoUM = YieldSupplyPromoUM( val bottomSheetNavigation: SlotNavigation<YieldSupplyPromoConfig> = SlotNavigation()
tosLink = AAVE_TOS_URL,
policyLink = AAVE_PRIVACY_URL, val uiState: YieldSupplyPromoUM = buildUiState()
tokenSymbol = params.currency.symbol,
title = resourceReference(
R.string.yield_module_promo_screen_title_v2,
wrappedList(params.apy),
),
subtitle = resourceReference(
R.string.yield_module_promo_screen_variable_rate_info_v2,
),
)
init { init {
analytics.send( analytics.send(
@ -51,10 +49,9 @@ internal class YieldSupplyPromoModel @Inject constructor(
blockchain = params.currency.network.name, blockchain = params.currency.network.name,
), ),
) )
modelScope.launch(dispatchers.io) { boostStoryPreloader.preload() }
} }
val bottomSheetNavigation: SlotNavigation<YieldSupplyPromoConfig> = SlotNavigation()
override fun onBackClick() { override fun onBackClick() {
appRouter.pop() appRouter.pop()
} }
@ -78,6 +75,31 @@ internal class YieldSupplyPromoModel @Inject constructor(
bottomSheetNavigation.activate(YieldSupplyPromoConfig.Action) bottomSheetNavigation.activate(YieldSupplyPromoConfig.Action)
} }
private fun buildUiState(): YieldSupplyPromoUM {
val isBoost = params.isPromoEnabled
val baseApyText = if (isBoost) "${params.apy}%" else null
val boostedApyText = if (isBoost) {
val baseApy = params.apy.toBigDecimalOrNull() ?: BigDecimal.ZERO
"${getBoostedApyUseCase(baseApy)}%"
} else {
null
}
return YieldSupplyPromoUM(
tosLink = AAVE_TOS_URL,
policyLink = AAVE_PRIVACY_URL,
boostTermsLink = TangemSiteUrlBuilder.YIELD_MODE_TERMS_URL,
tokenSymbol = params.currency.symbol,
isBoostAvailable = isBoost,
baseApy = baseApyText,
boostedApy = boostedApyText,
title = resourceReference(
R.string.yield_module_promo_screen_title_v2,
wrappedList(params.apy),
),
subtitle = resourceReference(R.string.yield_module_promo_screen_variable_rate_info_v2),
)
}
private companion object { private companion object {
const val AAVE_TOS_URL = "https://aave.com/terms-of-service" const val AAVE_TOS_URL = "https://aave.com/terms-of-service"
const val AAVE_PRIVACY_URL = "https://aave.com/privacy-policy" const val AAVE_PRIVACY_URL = "https://aave.com/privacy-policy"

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -16,12 +17,17 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.* import com.tangem.core.ui.components.*
@ -73,7 +79,7 @@ internal fun YieldSupplyPromoContent(
} }
} }
@Suppress("MagicNumber") @Suppress("MagicNumber", "LongMethod")
@Composable @Composable
private fun ColumnScope.Content(yieldSupplyPromoUM: YieldSupplyPromoUM, clickIntents: YieldSupplyPromoClickIntents) { private fun ColumnScope.Content(yieldSupplyPromoUM: YieldSupplyPromoUM, clickIntents: YieldSupplyPromoClickIntents) {
Box(modifier = Modifier.weight(1f)) { Box(modifier = Modifier.weight(1f)) {
@ -98,12 +104,22 @@ private fun ColumnScope.Content(yieldSupplyPromoUM: YieldSupplyPromoUM, clickInt
.size(32.dp), .size(32.dp),
) )
SpacerH(20.dp) SpacerH(20.dp)
Text( if (yieldSupplyPromoUM.isBoostAvailable &&
text = yieldSupplyPromoUM.title.resolveReference(), yieldSupplyPromoUM.baseApy != null &&
style = TangemTheme.typography.h2, yieldSupplyPromoUM.boostedApy != null
textAlign = TextAlign.Center, ) {
color = TangemTheme.colors.text.primary1, BoostPromoTitle(
) baseApy = yieldSupplyPromoUM.baseApy,
boostedApy = yieldSupplyPromoUM.boostedApy,
)
} else {
Text(
text = yieldSupplyPromoUM.title.resolveReference(),
style = TangemTheme.typography.h2,
textAlign = TextAlign.Center,
color = TangemTheme.colors.text.primary1,
)
}
SpacerH8() SpacerH8()
Label( Label(
state = LabelUM( state = LabelUM(
@ -117,6 +133,17 @@ private fun ColumnScope.Content(yieldSupplyPromoUM: YieldSupplyPromoUM, clickInt
SpacerH32() SpacerH32()
PromoItems(yieldSupplyPromoUM.tokenSymbol) PromoItems(yieldSupplyPromoUM.tokenSymbol)
} }
if (yieldSupplyPromoUM.isBoostAvailable &&
yieldSupplyPromoUM.baseApy != null &&
yieldSupplyPromoUM.boostedApy != null
) {
SpacerH(20.dp)
PromoBoostCard(
baseApy = yieldSupplyPromoUM.baseApy,
boostedApy = yieldSupplyPromoUM.boostedApy,
onLearnMoreClick = { clickIntents.onUrlClick(yieldSupplyPromoUM.boostTermsLink) },
)
}
SpacerH32() SpacerH32()
} }
Fade( Fade(
@ -176,6 +203,102 @@ private fun PromoItems(tokenSymbol: String) {
) )
} }
@Suppress("MagicNumber")
@Composable
private fun BoostPromoTitle(baseApy: String, boostedApy: String) {
val accent = TangemTheme.colors.text.accent
val primary = TangemTheme.colors.text.primary1
// Pass `%1$s` back as the argument so the placeholder survives formatting (`%%` → `%`).
val raw = stringResourceSafe(R.string.yield_module_promo_screen_title_v2, "%1\$s")
val (head, rest) = raw.split("%1\$s", limit = 2)
// The template leaves a stray `%` right after the value (after a space in RU/UK), but the APY
// strings already carry their own `%` — drop that duplicate.
val tail = rest.trimStart().removePrefix("%")
val annotated = buildAnnotatedString {
append(head)
withStyle(SpanStyle(color = accent, textDecoration = TextDecoration.LineThrough)) {
append(baseApy)
}
// Arrow glyph sits lower than digits in most fonts; lift it onto the cap-height baseline.
withStyle(SpanStyle(color = accent, baselineShift = BaselineShift(0.1f))) {
append("")
}
withStyle(SpanStyle(color = accent)) {
append(boostedApy)
}
append(tail)
}
Text(
text = annotated,
style = TangemTheme.typography.h2,
textAlign = TextAlign.Center,
color = primary,
)
}
@Composable
private fun PromoBoostCard(baseApy: String, boostedApy: String, onLearnMoreClick: () -> Unit) {
val accent = TangemTheme.colors.text.accent
val primary = TangemTheme.colors.text.primary1
val tertiary = TangemTheme.colors.text.tertiary
val titleAnnotated = buildAnnotatedString {
withStyle(SpanStyle(color = primary)) {
append(stringResourceSafe(R.string.common_yield_mode))
append(" · ")
}
withStyle(SpanStyle(color = accent)) {
append("APY ")
}
withStyle(SpanStyle(color = accent, textDecoration = TextDecoration.LineThrough)) {
append(baseApy)
}
withStyle(SpanStyle(color = accent)) {
append(" x3 → ")
append(boostedApy)
}
}
val learnMoreLabel = stringResourceSafe(R.string.common_learn_more).lowercase()
val eligibilityText = stringResourceSafe(R.string.yield_apy_boost_promo_eligibility_text)
val subtitleAnnotated = buildAnnotatedString {
append(eligibilityText)
append(" ")
withLink(
link = LinkAnnotation.Clickable(
tag = "YIELD_BOOST_LEARN_MORE",
linkInteractionListener = { onLearnMoreClick() },
),
block = {
appendColored(text = learnMoreLabel, color = accent)
},
)
}
Row(
verticalAlignment = Alignment.Top,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.primary)
.padding(12.dp),
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_gift_promo_24),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
Column(
modifier = Modifier.padding(start = 12.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(text = titleAnnotated, style = TangemTheme.typography.subtitle2)
Text(
text = subtitleAnnotated,
style = TangemTheme.typography.caption2,
color = tertiary,
)
}
}
}
@Composable @Composable
private fun PromoItem(@DrawableRes icon: Int, title: TextReference, subtitle: TextReference) { private fun PromoItem(@DrawableRes icon: Int, title: TextReference, subtitle: TextReference) {
Row( Row(
@ -262,9 +385,11 @@ private fun YieldSupplyPromoContent_Preview() {
yieldSupplyPromoUM = YieldSupplyPromoUM( yieldSupplyPromoUM = YieldSupplyPromoUM(
tosLink = "https://tangem.com/terms-of-service/", tosLink = "https://tangem.com/terms-of-service/",
policyLink = "https://tangem.com/privacy-policy/", policyLink = "https://tangem.com/privacy-policy/",
boostTermsLink = "https://tangem.com/docs/en/yield-mode-terms.pdf",
title = resourceReference(R.string.yield_module_promo_screen_title), title = resourceReference(R.string.yield_module_promo_screen_title),
tokenSymbol = "USDT", tokenSymbol = "USDT",
subtitle = resourceReference(R.string.yield_module_promo_screen_variable_rate_info, wrappedList("5.3")), subtitle = resourceReference(R.string.yield_module_promo_screen_variable_rate_info, wrappedList("5.3")),
isBoostAvailable = false,
), ),
clickIntents = object : YieldSupplyPromoClickIntents { clickIntents = object : YieldSupplyPromoClickIntents {
override fun onBackClick() {} override fun onBackClick() {}

View file

@ -155,6 +155,7 @@ internal class YieldSupplyToEarnBlockConverterTest {
apyText = stringReference("5.1 % APY"), apyText = stringReference("5.1 % APY"),
title = stringReference("Yield Mode"), title = stringReference("Yield Mode"),
onClick = { clicked = true }, onClick = { clicked = true },
onLearnMoreClick = {},
) )
val result = converter.convert(available) val result = converter.convert(available)