Updated on 2026-08-14
This commit is contained in:
parent
ff8f9c9b18
commit
6aab25427d
75 changed files with 2122 additions and 57 deletions
|
|
@ -10,6 +10,11 @@ import com.tangem.domain.transaction.error.FeeErrorResolver
|
|||
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
|
||||
import com.tangem.domain.yield.supply.YieldSupplyRepository
|
||||
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.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -260,4 +265,32 @@ internal object YieldSupplyDomainModule {
|
|||
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
|
||||
}
|
||||
|
|
@ -290,6 +290,7 @@ internal class ChildFactory @Inject constructor(
|
|||
storyId = route.storyId,
|
||||
nextScreen = route.nextScreen,
|
||||
screenSource = route.screenSource,
|
||||
shouldMarkAsSeenOnClose = route.shouldMarkAsSeenOnClose,
|
||||
),
|
||||
componentFactory = storiesComponentFactory,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -350,6 +350,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
val storyId: String,
|
||||
val nextScreen: AppRoute? = null,
|
||||
val screenSource: String,
|
||||
val shouldMarkAsSeenOnClose: Boolean = true,
|
||||
) : AppRoute(path = "/stories$storyId")
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ object TangemSiteUrlBuilder {
|
|||
const val HELP_CENTER_SWAP_URL =
|
||||
"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 {
|
||||
val langCode = Locale.getDefault().language
|
||||
val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty()
|
||||
|
|
|
|||
|
|
@ -94,5 +94,9 @@
|
|||
{
|
||||
"name": "AND_15122_SWAP_PREDEFINED_BUTTONS_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15154_YIELD_PROMO_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.datasource.api.tangemTech
|
||||
|
||||
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.tangemTech.models.*
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
|
|
@ -118,6 +120,18 @@ interface TangemTechApi {
|
|||
@GET("v1/stories/{story_id}")
|
||||
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
|
||||
@GET("v1/notification/push_notifications_eligible_networks")
|
||||
suspend fun getEligibleNetworksForPushNotifications(): ApiResponse<List<CryptoNetworkResponse>>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,13 @@ import androidx.datastore.core.DataStoreFactory
|
|||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
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.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.listTypes
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -403,6 +403,7 @@
|
|||
<string name="common_sending">Senden</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_session_expired">Sitzung abgelaufen</string>
|
||||
<string name="common_share">Teilen</string>
|
||||
<string name="common_share_link">Link teilen</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_ungroup">Gruppe löschen</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_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_offers_updates_subtitle">Produktneuheiten, exklusive Angebote und Erinnerungen an Aktivitäten.</string>
|
||||
<string name="push_notification_settings_offers_updates_title">Angebote & Updates</string>
|
||||
|
|
@ -1853,6 +1856,10 @@
|
|||
<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_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_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>
|
||||
|
|
@ -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_apy_multiplied">APY x3</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&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_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_title">Yield Mode nicht verfügbar</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>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@
|
|||
<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_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_title">Agregue un token personalizado</string>
|
||||
<string name="add_tokens_title">Gestionar tokens</string>
|
||||
|
|
@ -569,6 +570,7 @@
|
|||
<string name="express_provider">Proveedor</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_for_swap">Proveedor de intercambio</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_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_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="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_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>
|
||||
|
|
@ -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_fetching_best_rates">Obteniendo las mejores tarifas...</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_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_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_offer_type_fastet">Procesamiento más rápido</string>
|
||||
<string name="onramp_pay_with">Pagar con</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_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">
|
||||
<item quantity="one">%d proveedor</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_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_detailed_mode">Modo detallado</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_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_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_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_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>
|
||||
|
|
@ -1552,6 +1570,10 @@
|
|||
<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_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_in_progress">Intercambiando...</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_temporarily_unavailable">Tangem Pay no está disponible temporalmente.</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_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>
|
||||
|
|
|
|||
|
|
@ -1497,6 +1497,10 @@
|
|||
<string name="swapping_high_price_impact_title">Impact élevé sur les prix</string>
|
||||
<string name="swapping_insufficient_funds">Fonds insuffisants</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_in_progress">Échange...</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_temporarily_unavailable">Tangem Pay est temporairement indisponible</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_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>
|
||||
|
|
|
|||
|
|
@ -218,6 +218,10 @@
|
|||
<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_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_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>
|
||||
|
|
|
|||
|
|
@ -1187,7 +1187,9 @@
|
|||
<string name="organize_tokens_title">トークンを整理する</string>
|
||||
<string name="organize_tokens_ungroup">グループ解除</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_grant_permission">プッシュ通知は有効になっていますが、許可するまで機能しません</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_title">オファー・最新情報</string>
|
||||
|
|
@ -1827,6 +1829,10 @@
|
|||
<string name="tangempay_tangem_visa_card">日常の支払いにUSDCを利用</string>
|
||||
<string name="tangempay_temporarily_unavailable">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_use_tangem_device_to_restore_payment_account">下のボタンをクリックしてアクセスを復元してください</string>
|
||||
<string name="tangempay_withdrawal_note_description">返金分はオンチェーンのPolygon残高には戻らず、出金にも利用できません。ただし、カード残高として残り、支払いに利用できます。</string>
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
<string name="common_sending">Enviando</string>
|
||||
<string name="common_sent">Enviado</string>
|
||||
<string name="common_server_unavailable">O servidor não está disponível. Tente novamente mais tarde.</string>
|
||||
<string name="common_session_expired">Sessão expirada</string>
|
||||
<string name="common_share">Compartilhar</string>
|
||||
<string name="common_share_link">Compartilhar link</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_ungroup">Desagrupar</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_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_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>
|
||||
|
|
@ -1853,6 +1856,10 @@
|
|||
<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_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_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>
|
||||
|
|
@ -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_apy_multiplied">APY x3</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_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>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
<string name="actionbutton_swap_title">Обмен</string>
|
||||
<string name="actionbutton_transfer_title">Перевод</string>
|
||||
<string name="add_and_manage_sheet_manage_subtitle">Добавить в портфель</string>
|
||||
<string name="add_and_manage_sheet_manage_title">Добавить токен</string>
|
||||
<string name="add_and_manage_sheet_manage_title">Добавить токены</string>
|
||||
<string name="add_and_manage_sheet_organize_subtitle">Сортировка и группировка</string>
|
||||
<string name="add_and_manage_sheet_organize_title">Упорядочить токены</string>
|
||||
<string name="add_custom_token_choose_network">Выберите сеть</string>
|
||||
|
|
@ -617,6 +617,7 @@
|
|||
<string name="express_provider">Провайдер</string>
|
||||
<string name="express_provider_best_rate">Лучший курс</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_max_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_level_description">Сеть Koinos использует Ману для оплаты комиссии сети. У вас есть %1$s/%2$s Mana</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_manage_tokens">Управление токенами</string>
|
||||
<string name="main_qr_scan_hint">Отсканируйте QR-код, чтобы отправить средства или подключиться к приложению.</string>
|
||||
|
|
@ -1160,15 +1161,26 @@
|
|||
<string name="onramp_error_transaction_already_processed">Эта транзакция уже была обработана. Дополнительных действий не требуется.</string>
|
||||
<string name="onramp_fetching_best_rates">Получение лучших курсов...</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_max_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_offer_type_fastet">Самый быстрый</string>
|
||||
<string name="onramp_pay_with">Оплата с</string>
|
||||
<string name="onramp_payment_method_subtitle">Платежный метод</string>
|
||||
<string name="onramp_provider_max_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">
|
||||
<item quantity="one">%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_title">Недостаточно средств</string>
|
||||
<string name="swap_approve_description">Подтверждая, вы разрешаете смарт-контракту использовать ваши токены в будущих транзакциях.</string>
|
||||
<string name="swap_detailed_mode">Детальный режим</string>
|
||||
<string name="swap_fixed_rate">Фиксированный курс</string>
|
||||
<string name="swap_give_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.</string>
|
||||
<string name="swap_in_progress">Обмен в процессе</string>
|
||||
|
|
@ -1595,6 +1608,7 @@
|
|||
<string name="swap_promo_title">Новый провайдер обмена!</string>
|
||||
<string name="swap_search_tooltip_description">Найдите любой токен, даже если его ещё нет в вашем списке</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_title">Круглосуточная поддержка</string>
|
||||
<string name="swap_story_first_subtitle">Надежные провайдеры в одном месте — обменивайте любые активы легко и быстро прямо в своем кошельке!</string>
|
||||
|
|
@ -1632,6 +1646,10 @@
|
|||
<string name="swapping_insufficient_funds">Недостаточно средств</string>
|
||||
<string name="swapping_insufficient_funds_description">Недостаточно средств для завершения этой транзакции. Уменьшите сумму для получения или добавьте больше средств.</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_in_progress">Обмен…</string>
|
||||
<string name="swapping_to_account_title">Вы получите на</string>
|
||||
|
|
@ -1803,6 +1821,10 @@
|
|||
<string name="tangempay_tangem_visa_card">Оплачивайте ежедневные покупки в USDC</string>
|
||||
<string name="tangempay_temporarily_unavailable">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_use_tangem_device_to_restore_payment_account">Нажмите на кнопку ниже, чтобы восстановить доступ</string>
|
||||
<string name="tangempay_withdrawal_note_description">При возвратах покупок средства не возвращаются на ончейн-баланс Polygon и недоступны для вывода, но отображаются на карте и могут быть использованы для покупок</string>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@
|
|||
<string name="action_buttons_swap_not_enough_tokens_alert_title">Додайте токени</string>
|
||||
<string name="action_buttons_you_want_to_receive">Оберіть токен для отримання</string>
|
||||
<string name="action_buttons_you_want_to_swap">Оберіть токен для обміну</string>
|
||||
<string name="add_and_manage_sheet_manage_title">Додати токени</string>
|
||||
<string name="add_custom_token_choose_network">Оберіть мережу</string>
|
||||
<string name="add_custom_token_title">Додати токен</string>
|
||||
<string name="add_tokens_title">Токени</string>
|
||||
|
|
@ -569,6 +570,7 @@
|
|||
<string name="express_provider">Провайдер</string>
|
||||
<string name="express_provider_best_rate">Найкращий курс</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_in_fca_warning_list">Список попереджень FCA</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_level_description">Мережа Koinos вимагає Mana для мережевої комісії. У вас є %1$s/%2$s 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_manage_tokens">Керування токенами</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_fetching_best_rates">Шукаємо найвигідніший курс...</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_text">Послуга надається зовнішнім провайдером.\nTangem не несе відповідальності.</string>
|
||||
<string name="onramp_max_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_offer_type_fastet">Найшвидший</string>
|
||||
<string name="onramp_pay_with">Оплата з</string>
|
||||
<string name="onramp_payment_method_subtitle">Спосіб оплати</string>
|
||||
<string name="onramp_provider_max_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">
|
||||
<item quantity="one">%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_title">Недостатньо коштів</string>
|
||||
<string name="swap_approve_description">Підтверджуючи, ви дозволяєте смартконтракту використовувати ваші токени в майбутніх транзакціях.</string>
|
||||
<string name="swap_detailed_mode">Детальний режим</string>
|
||||
<string name="swap_fixed_rate">Фіксований курс</string>
|
||||
<string name="swap_give_permission_fee_footer">Мережа стягує комісію за схвалення токену за підтвердження, що саме ви дозволяєте використовувати ваш токен для обміну.</string>
|
||||
<string name="swap_in_progress">Обмін у процесі</string>
|
||||
|
|
@ -1517,6 +1532,7 @@
|
|||
<string name="swap_search_suggestion_hint">Шукаєте щось інше?\nСпробуйте пошукати або перегляньте інші криптовалюти!</string>
|
||||
<string name="swap_search_tooltip_description">Шукайте будь-який токен, навіть якщо його ще немає у вашому списку.</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_title">Цілодобова підтримка</string>
|
||||
<string name="swap_story_first_subtitle">Надійні провайдери дозволяють легко обмінювати активи, забезпечуючи повну безпеку у вашому гаманці</string>
|
||||
|
|
@ -1551,6 +1567,10 @@
|
|||
<string name="swapping_high_price_impact_title">Високий вплив на ціну</string>
|
||||
<string name="swapping_insufficient_funds">Недостатньо коштів</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_in_progress">Обмін...</string>
|
||||
<string name="swapping_to_account_title">Ви отримаєте на</string>
|
||||
|
|
@ -1720,6 +1740,10 @@
|
|||
<string name="tangempay_tangem_visa_card">Використовуйте USDC для щоденних платежів</string>
|
||||
<string name="tangempay_temporarily_unavailable">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_use_tangem_device_to_restore_payment_account">Натисніть кнопку нижче, щоб відновити доступ</string>
|
||||
<string name="tangempay_withdrawal_note_description">Кошти з повернених покупок не будуть повернуті на ваш ончейн-баланс Polygon і не будуть доступні для виведення, але залишаться на балансі картки для покупок.</string>
|
||||
|
|
|
|||
|
|
@ -1187,7 +1187,9 @@
|
|||
<string name="organize_tokens_title">整理代币</string>
|
||||
<string name="organize_tokens_ungroup">取消分组</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_grant_permission">推送通知已启用,但需要您授予权限才能生效。</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_title">优惠与更新</string>
|
||||
|
|
@ -1827,6 +1829,10 @@
|
|||
<string name="tangempay_tangem_visa_card">使用 USDC 进行日常支付</string>
|
||||
<string name="tangempay_temporarily_unavailable">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_use_tangem_device_to_restore_payment_account">点击下方按钮恢复访问权限</string>
|
||||
<string name="tangempay_withdrawal_note_description">您的Polygon链上 USDC 余额与您的卡片余额不同,并在购买后 2 个工作日内更新。购物退款的资金不会退还至您的链上余额,也不能提现,但会保留在您的卡片余额中用于购物。</string>
|
||||
|
|
|
|||
|
|
@ -435,6 +435,10 @@
|
|||
<string name="tangempay_tangem_visa_card">使用 USDC 進行日常支付</string>
|
||||
<string name="tangempay_temporarily_unavailable">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_withdrawal_note_description">您的 USDC Polygon 鏈上餘額與卡片餘額不同,並在購買後 2 個工作日內更新。退款交易的資金不會返回到您的鏈上餘額或可供提現,但會保留在您的卡片餘額中用於購買。</string>
|
||||
<string name="tangempay_withdrawal_note_title">請注意</string>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import androidx.compose.ui.unit.Dp
|
|||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
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.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
|
|
@ -259,7 +260,9 @@ internal fun TextsBlock(
|
|||
titleColor: Color = TangemTheme.colors.text.primary1,
|
||||
) {
|
||||
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) {
|
||||
Text(
|
||||
|
|
|
|||
18
core/ui/src/main/res/drawable/ic_gift_promo_24.xml
Normal file
18
core/ui/src/main/res/drawable/ic_gift_promo_24.xml
Normal 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>
|
||||
|
|
@ -43,6 +43,7 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.datetime)
|
||||
|
||||
/** tests */
|
||||
testImplementation(projects.common.test)
|
||||
|
|
|
|||
|
|
@ -4,13 +4,18 @@ import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
|||
import com.tangem.data.yield.supply.DefaultYieldSupplyRepository
|
||||
import com.tangem.data.yield.supply.DefaultYieldSupplyErrorResolver
|
||||
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.local.preferences.AppPreferencesStore
|
||||
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.yield.supply.YieldSupplyRepository
|
||||
import com.tangem.domain.yield.supply.YieldSupplyErrorResolver
|
||||
import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository
|
||||
import com.tangem.domain.yield.supply.promo.YieldPromoRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -59,4 +64,20 @@ internal object YieldSupplyDataModule {
|
|||
fun provideYieldSupplyErrorResolver(): YieldSupplyErrorResolver {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -25,4 +25,5 @@ data class StoryContent(
|
|||
|
||||
enum class StoryContentIds(val id: String, val analyticType: String) {
|
||||
STORY_FIRST_TIME_SWAP(id = "first-time-swap-v2", analyticType = "Swap"),
|
||||
STORY_FIRST_TIME_YIELD_PROMO(id = "first-time-yield-promo", analyticType = "YieldPromo"),
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ dependencies {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.libs.crypto)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.account.status)
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@ dependencies {
|
|||
|
||||
// region Other libraries
|
||||
implementation(deps.kotlin.serialization)
|
||||
api(deps.kotlin.datetime)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ interface StoriesComponent : ComposableContentComponent {
|
|||
val storyId: String,
|
||||
val nextScreen: AppRoute? = null,
|
||||
val screenSource: String,
|
||||
val shouldMarkAsSeenOnClose: Boolean = true,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, StoriesComponent>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.stories.impl
|
||||
|
||||
import com.tangem.domain.stories.models.StoryContentIds
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
@ -13,6 +14,7 @@ internal object StoriesSlideConfigs {
|
|||
|
||||
fun getSlides(storyId: String): ImmutableList<SlideConfig> = when (storyId) {
|
||||
StoryContentIds.STORY_FIRST_TIME_SWAP.id -> swapSlides()
|
||||
StoryContentIds.STORY_FIRST_TIME_YIELD_PROMO.id -> yieldPromoSlides()
|
||||
else -> persistentListOf()
|
||||
}
|
||||
|
||||
|
|
@ -34,4 +36,23 @@ internal object StoriesSlideConfigs {
|
|||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ internal class StoriesModel @Inject constructor(
|
|||
|
||||
private fun openScreen(hideStories: Boolean = true) {
|
||||
modelScope.launch {
|
||||
if (hideStories) {
|
||||
if (hideStories && params.shouldMarkAsSeenOnClose) {
|
||||
shouldShowStoriesUseCase.neverToShow(params.storyId)
|
||||
}
|
||||
router.pop()
|
||||
|
|
|
|||
|
|
@ -33,8 +33,10 @@ import com.tangem.domain.settings.NeverToSuggestRateAppUseCase
|
|||
import com.tangem.domain.settings.RemindToRateAppLaterUseCase
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
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.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.MainScreen
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
|
|
@ -87,6 +89,10 @@ internal interface WalletWarningsClickIntents {
|
|||
fun onDismissAssetsDiscoveryNotification(userWalletId: UserWalletId)
|
||||
|
||||
fun onAssetsDiscoveryManageClick(userWalletId: UserWalletId)
|
||||
|
||||
fun onYieldBoostBannerClick(userWalletId: UserWalletId)
|
||||
|
||||
fun onDismissYieldBoostBanner(userWalletId: UserWalletId)
|
||||
}
|
||||
|
||||
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
|
||||
|
|
@ -118,6 +124,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
private val reviewManager: ReviewManager,
|
||||
private val closeHotWalletUpgradeBannerUseCase: CloseHotWalletUpgradeBannerUseCase,
|
||||
private val acknowledgeAssetsDiscoveryCompletionUseCase: AcknowledgeAssetsDiscoveryCompletionUseCase,
|
||||
private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase,
|
||||
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
|
||||
|
||||
override fun onAddBackupCardClick() {
|
||||
|
|
@ -408,4 +415,21 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -98,6 +98,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
|
|||
is WalletNotification.Warning.TangemPayRefreshNeeded -> null
|
||||
is WalletNotification.Warning.TangemPayUnreachable -> null
|
||||
is WalletNotification.UpgradeHotWalletPromo -> null
|
||||
is WalletNotification.YieldBoostPromo -> null
|
||||
is WalletNotification.AssetsDiscoveryCompleted -> null
|
||||
is WalletNotification.CreateTangemPayAccount -> TangemPayAnalyticsEvents.PermanentBannerShowed()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package com.tangem.feature.wallet.presentation.wallet.domain
|
|||
import com.tangem.common.ui.notifications.NotificationId
|
||||
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
|
||||
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.extensions.resourceReference
|
||||
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.usecase.ObserveAssetsDiscoveryUseCase
|
||||
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.features.yield.supply.api.YieldSupplyFeatureToggles
|
||||
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.account.AccountDependencies
|
||||
|
|
@ -60,6 +64,10 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase,
|
||||
private val observeAssetsDiscoveryUseCase: ObserveAssetsDiscoveryUseCase,
|
||||
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")
|
||||
|
|
@ -87,6 +95,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
getUpgradeBannerClosureTimestampUseCase(userWallet.walletId)
|
||||
.distinctUntilChanged(),
|
||||
assetsDiscoveryProgressFlow,
|
||||
yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged(),
|
||||
) { array -> array }
|
||||
.map { array ->
|
||||
val accountStatusList = array[0] as AccountStatusList
|
||||
|
|
@ -97,6 +106,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
val shouldShowUpgradeBanner = array[5] as Boolean
|
||||
val closureTimestamp = array[6] as? Long
|
||||
val assetsDiscoveryProgress = array[7] as AssetsDiscoveryProgress
|
||||
val shouldShowYieldBoostPromoLocal = array[8] as Boolean
|
||||
|
||||
val flattenCurrencies = accountStatusList.flattenCurrencies()
|
||||
val paymentAccountStatus = accountStatusList.accountStatuses
|
||||
|
|
@ -165,10 +175,34 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
walletClickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
|
||||
addYieldBoostBannerNotification(
|
||||
userWallet = userWallet,
|
||||
shouldShowLocal = shouldShowYieldBoostPromoLocal,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}.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(
|
||||
status: AccountStatus.Payment,
|
||||
userWallet: UserWallet,
|
||||
|
|
|
|||
|
|
@ -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.pluralReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
|
||||
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(
|
||||
val onCloseClick: () -> Unit,
|
||||
val onManageTokensClick: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.subscribers
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.stories.GetStoryContentUseCase
|
||||
import com.tangem.domain.stories.models.StoryContentIds
|
||||
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
|
||||
|
|
@ -15,7 +17,9 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]")
|
||||
internal class MultiWalletWarningsSubscriber @AssistedInject constructor(
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
|
|
@ -24,6 +28,7 @@ internal class MultiWalletWarningsSubscriber @AssistedInject constructor(
|
|||
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
|
||||
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
||||
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
|
||||
private val getStoryContentUseCase: GetStoryContentUseCase,
|
||||
) : WalletSubscriber() {
|
||||
|
||||
override fun create(coroutineScope: CoroutineScope): Flow<ImmutableList<WalletNotification>> {
|
||||
|
|
@ -31,6 +36,15 @@ internal class MultiWalletWarningsSubscriber @AssistedInject constructor(
|
|||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.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)
|
||||
|
||||
// Wait until the wallet appears in the list
|
||||
|
|
|
|||
|
|
@ -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.items
|
||||
import androidx.compose.runtime.Composable
|
||||
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.core.ui.components.notifications.NoteMigrationNotification
|
||||
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.stringResourceSafe
|
||||
import com.tangem.core.ui.res.ForceDarkTheme
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
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 -> {
|
||||
CreatePaymentAccountNotification(
|
||||
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
|
|
@ -77,3 +92,17 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.yield.supply.api
|
||||
|
||||
interface YieldSupplyFeatureToggles {
|
||||
val isYieldPromoEnabled: Boolean
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ interface YieldSupplyPromoComponent : ComposableContentComponent {
|
|||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
val apy: String,
|
||||
val isPromoEnabled: Boolean = false,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, YieldSupplyPromoComponent>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ sealed class YieldSupplyEntryRoute : Route {
|
|||
data class Promo(
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val apy: String,
|
||||
val isPromoEnabled: Boolean = false,
|
||||
) : YieldSupplyEntryRoute()
|
||||
|
||||
/** Route to yield supply active screen */
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ dependencies {
|
|||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.yieldSupply.models)
|
||||
implementation(projects.domain.yieldSupply)
|
||||
implementation(projects.domain.stories.models)
|
||||
implementation(projects.domain.stories)
|
||||
implementation(projects.domain.feedback.models)
|
||||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
|
|
@ -76,6 +78,7 @@ dependencies {
|
|||
implementation(deps.decompose)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.kotlin.datetime)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,4 +17,6 @@ internal data class YieldSupplyActiveContentUM(
|
|||
val minFeeDescription: TextReference?,
|
||||
val apy: TextReference? = null,
|
||||
val isHighFee: Boolean = false,
|
||||
val boostText: TextReference? = null,
|
||||
val onBoostClick: () -> Unit = {},
|
||||
)
|
||||
|
|
@ -11,7 +11,10 @@ import com.tangem.core.decompose.di.ModelScoped
|
|||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
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.stringReference
|
||||
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.wallet.UserWallet
|
||||
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.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.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.model.transformers.YieldSupplyActiveFeeContentTransformer
|
||||
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.stopearning.YieldSupplyStopEarningComponent
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
|
@ -41,7 +52,10 @@ import com.tangem.utils.transformer.update
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@ModelScoped
|
||||
|
|
@ -60,6 +74,10 @@ internal class YieldSupplyActiveModel @Inject constructor(
|
|||
private val urlOpener: UrlOpener,
|
||||
private val appRouter: AppRouter,
|
||||
private val yieldSupplyGetDustMinAmountUseCase: YieldSupplyGetDustMinAmountUseCase,
|
||||
private val getYieldBoostStatusUseCase: GetYieldBoostStatusUseCase,
|
||||
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
|
||||
private val designFeatureToggles: DesignFeatureToggles,
|
||||
private val boostStoryPreloader: YieldBoostStoryPreloader,
|
||||
) : Model(), YieldSupplyStopEarningComponent.ModelCallback,
|
||||
YieldSupplyApproveComponent.ModelCallback {
|
||||
|
||||
|
|
@ -112,6 +130,8 @@ internal class YieldSupplyActiveModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
subscribeOnCurrencyStatusUpdates()
|
||||
loadBoostBlock()
|
||||
modelScope.launch(dispatchers.io) { boostStoryPreloader.preload() }
|
||||
|
||||
modelScope.launch(dispatchers.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() {
|
||||
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
|
||||
modelScope.launch(dispatchers.default) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedContent
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
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 kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun YieldSupplyActiveContent(
|
||||
state: YieldSupplyActiveContentUM,
|
||||
|
|
@ -61,16 +63,30 @@ internal fun YieldSupplyActiveContent(
|
|||
),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
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()) {
|
||||
val notifications = remember(state.notifications) { state.notifications }
|
||||
|
|
@ -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
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -86,6 +86,7 @@ internal class DefaultYieldSupplyEntryComponent @AssistedInject constructor(
|
|||
userWalletId = params.userWalletId,
|
||||
currency = configuration.cryptoCurrency,
|
||||
apy = configuration.apy,
|
||||
isPromoEnabled = configuration.isPromoEnabled,
|
||||
),
|
||||
)
|
||||
is YieldSupplyEntryRoute.Active -> yieldSupplyActiveComponentFactory.create(
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
package com.tangem.features.yield.supply.impl.entry.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
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.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.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -20,12 +24,16 @@ import com.tangem.utils.logging.TangemLogger
|
|||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class YieldSupplyEntryModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val isYieldBoostPromoEnabledForTokenUseCase: IsYieldBoostPromoEnabledForTokenUseCase,
|
||||
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
|
||||
private val designFeatureToggles: DesignFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<YieldSupplyEntryComponent.Params>()
|
||||
|
|
@ -90,7 +98,14 @@ internal class YieldSupplyEntryModel @Inject constructor(
|
|||
return if (isActiveYield) {
|
||||
YieldSupplyEntryRoute.Active(cryptoCurrency = token)
|
||||
} 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ internal sealed class YieldSupplyUM {
|
|||
val apyText: TextReference,
|
||||
val title: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
val onLearnMoreClick: () -> Unit,
|
||||
val isBoostAvailable: Boolean = false,
|
||||
) : YieldSupplyUM()
|
||||
|
||||
data object Loading : YieldSupplyUM()
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ package com.tangem.features.yield.supply.impl.main.model
|
|||
interface YieldSupplyClickIntents {
|
||||
fun onStartEarningClick()
|
||||
fun onActiveClick()
|
||||
fun onLearnMoreClick()
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
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.yield.supply.YieldSupplyStatus
|
||||
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.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.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.impl.R
|
||||
import com.tangem.features.yield.supply.impl.YieldBoostStoryPreloader
|
||||
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.model.converter.YieldSupplyToEarnBlockConverter
|
||||
|
|
@ -61,6 +67,11 @@ internal class YieldSupplyModel @Inject constructor(
|
|||
private val yieldSupplyEnterStatusFlowUseCase: YieldSupplyEnterStatusFlowUseCase,
|
||||
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
|
||||
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 {
|
||||
|
||||
private val earnBlockConverter = YieldSupplyToEarnBlockConverter()
|
||||
|
|
@ -82,6 +93,7 @@ internal class YieldSupplyModel @Inject constructor(
|
|||
|
||||
init {
|
||||
checkIfYieldSupplyIsAvailable()
|
||||
modelScope.launch(dispatchers.io) { boostStoryPreloader.preload() }
|
||||
}
|
||||
|
||||
private fun checkIfYieldSupplyIsAvailable() {
|
||||
|
|
@ -150,10 +162,17 @@ internal class YieldSupplyModel @Inject constructor(
|
|||
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
|
||||
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
|
||||
.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(
|
||||
YieldSupplyTokenStatusSuccessTransformer(
|
||||
tokenStatus = tokenStatus,
|
||||
onStartEarningClick = ::onStartEarningClick,
|
||||
onLearnMoreClick = ::onLearnMoreClick,
|
||||
boostedApy = boostedApy,
|
||||
),
|
||||
)
|
||||
}.onLeft { error ->
|
||||
|
|
@ -170,19 +189,33 @@ internal class YieldSupplyModel @Inject constructor(
|
|||
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() {
|
||||
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) {
|
||||
is YieldSupplyUM.Available -> yieldSupplyUM.apy
|
||||
is YieldSupplyUM.Content -> yieldSupplyUM.apy
|
||||
else -> ""
|
||||
}
|
||||
appRouter.push(
|
||||
AppRoute.YieldSupplyEntry(
|
||||
return AppRoute.YieldSupplyEntry(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
apy = apy,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
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.resourceReference
|
||||
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.main.entity.YieldSupplyUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class YieldSupplyTokenStatusSuccessTransformer(
|
||||
private val tokenStatus: YieldMarketToken,
|
||||
private val onStartEarningClick: () -> Unit,
|
||||
private val onLearnMoreClick: () -> Unit,
|
||||
private val boostedApy: BigDecimal? = null,
|
||||
) : Transformer<YieldSupplyUM> {
|
||||
|
||||
override fun transform(prevState: YieldSupplyUM): YieldSupplyUM {
|
||||
if (!tokenStatus.isActive) return YieldSupplyUM.Unavailable
|
||||
|
||||
val boost = boostedApy
|
||||
return YieldSupplyUM.Available(
|
||||
title = resourceReference(
|
||||
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
|
||||
),
|
||||
title = if (boost != null) {
|
||||
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,
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
isBoostAvailable = boost != null,
|
||||
apy = tokenStatus.apy.toString(),
|
||||
apyText = combinedReference(
|
||||
resourceReference(
|
||||
R.string.yield_module_token_details_earn_notification_apy,
|
||||
),
|
||||
apyText = if (boost != null) {
|
||||
annotatedReference(buildBoostedApyText(baseApy = tokenStatus.apy, boostedApy = boost))
|
||||
} else {
|
||||
combinedReference(
|
||||
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%")
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.components.SpacerW8
|
||||
|
|
@ -64,8 +65,13 @@ internal fun YieldSupplyBlockContentLegacy(yieldSupplyUM: YieldSupplyUM, modifie
|
|||
|
||||
@Composable
|
||||
private fun SupplyAvailable(supplyUM: YieldSupplyUM.Available, modifier: Modifier = Modifier) {
|
||||
if (supplyUM.isBoostAvailable) {
|
||||
SupplyAvailableBoosted(supplyUM = supplyUM, modifier = modifier)
|
||||
} else {
|
||||
SupplyInfo(
|
||||
title = resourceReference(R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title),
|
||||
title = resourceReference(
|
||||
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
|
||||
),
|
||||
subtitle = resourceReference(R.string.yield_module_token_details_earn_notification_description),
|
||||
rewardsApy = supplyUM.apyText,
|
||||
iconTint = TangemTheme.colors.icon.accent,
|
||||
|
|
@ -80,6 +86,71 @@ private fun SupplyAvailable(supplyUM: YieldSupplyUM.Available, modifier: Modifie
|
|||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
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,
|
||||
size = TangemButtonSize.WideAction,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
|
|
@ -345,6 +416,15 @@ private class PreviewProvider : PreviewParameterProvider<YieldSupplyUM> {
|
|||
apy = "5.1",
|
||||
apyText = stringReference("5.1 % APY"),
|
||||
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(
|
||||
title = stringReference("Aave l"),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
data class YieldSupplyPromoUM(
|
||||
val tosLink: String,
|
||||
val policyLink: String,
|
||||
val boostTermsLink: String,
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
val tokenSymbol: String,
|
||||
val isBoostAvailable: Boolean = false,
|
||||
val baseApy: String? = null,
|
||||
val boostedApy: String? = null,
|
||||
)
|
||||
|
|
@ -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.activate
|
||||
import com.tangem.common.TangemBlogUrlBuilder
|
||||
import com.tangem.common.TangemSiteUrlBuilder
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.ui.extensions.resourceReference
|
||||
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.analytics.YieldSupplyAnalytics
|
||||
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.entity.YieldSupplyPromoUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class YieldSupplyPromoModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -27,22 +32,15 @@ internal class YieldSupplyPromoModel @Inject constructor(
|
|||
private val analytics: AnalyticsEventHandler,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val appRouter: AppRouter,
|
||||
private val getBoostedApyUseCase: GetBoostedApyUseCase,
|
||||
private val boostStoryPreloader: YieldBoostStoryPreloader,
|
||||
) : Model(), YieldSupplyPromoClickIntents {
|
||||
|
||||
val params: YieldSupplyPromoComponent.Params = paramsContainer.require()
|
||||
|
||||
val uiState: YieldSupplyPromoUM = YieldSupplyPromoUM(
|
||||
tosLink = AAVE_TOS_URL,
|
||||
policyLink = AAVE_PRIVACY_URL,
|
||||
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,
|
||||
),
|
||||
)
|
||||
val bottomSheetNavigation: SlotNavigation<YieldSupplyPromoConfig> = SlotNavigation()
|
||||
|
||||
val uiState: YieldSupplyPromoUM = buildUiState()
|
||||
|
||||
init {
|
||||
analytics.send(
|
||||
|
|
@ -51,10 +49,9 @@ internal class YieldSupplyPromoModel @Inject constructor(
|
|||
blockchain = params.currency.network.name,
|
||||
),
|
||||
)
|
||||
modelScope.launch(dispatchers.io) { boostStoryPreloader.preload() }
|
||||
}
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<YieldSupplyPromoConfig> = SlotNavigation()
|
||||
|
||||
override fun onBackClick() {
|
||||
appRouter.pop()
|
||||
}
|
||||
|
|
@ -78,6 +75,31 @@ internal class YieldSupplyPromoModel @Inject constructor(
|
|||
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 {
|
||||
const val AAVE_TOS_URL = "https://aave.com/terms-of-service"
|
||||
const val AAVE_PRIVACY_URL = "https://aave.com/privacy-policy"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -16,12 +17,17 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
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.TextDecoration
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.*
|
||||
|
|
@ -73,7 +79,7 @@ internal fun YieldSupplyPromoContent(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Suppress("MagicNumber", "LongMethod")
|
||||
@Composable
|
||||
private fun ColumnScope.Content(yieldSupplyPromoUM: YieldSupplyPromoUM, clickIntents: YieldSupplyPromoClickIntents) {
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
|
|
@ -98,12 +104,22 @@ private fun ColumnScope.Content(yieldSupplyPromoUM: YieldSupplyPromoUM, clickInt
|
|||
.size(32.dp),
|
||||
)
|
||||
SpacerH(20.dp)
|
||||
if (yieldSupplyPromoUM.isBoostAvailable &&
|
||||
yieldSupplyPromoUM.baseApy != null &&
|
||||
yieldSupplyPromoUM.boostedApy != null
|
||||
) {
|
||||
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()
|
||||
Label(
|
||||
state = LabelUM(
|
||||
|
|
@ -117,6 +133,17 @@ private fun ColumnScope.Content(yieldSupplyPromoUM: YieldSupplyPromoUM, clickInt
|
|||
SpacerH32()
|
||||
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()
|
||||
}
|
||||
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
|
||||
private fun PromoItem(@DrawableRes icon: Int, title: TextReference, subtitle: TextReference) {
|
||||
Row(
|
||||
|
|
@ -262,9 +385,11 @@ private fun YieldSupplyPromoContent_Preview() {
|
|||
yieldSupplyPromoUM = YieldSupplyPromoUM(
|
||||
tosLink = "https://tangem.com/terms-of-service/",
|
||||
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),
|
||||
tokenSymbol = "USDT",
|
||||
subtitle = resourceReference(R.string.yield_module_promo_screen_variable_rate_info, wrappedList("5.3")),
|
||||
isBoostAvailable = false,
|
||||
),
|
||||
clickIntents = object : YieldSupplyPromoClickIntents {
|
||||
override fun onBackClick() {}
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ internal class YieldSupplyToEarnBlockConverterTest {
|
|||
apyText = stringReference("5.1 % APY"),
|
||||
title = stringReference("Yield Mode"),
|
||||
onClick = { clicked = true },
|
||||
onLearnMoreClick = {},
|
||||
)
|
||||
|
||||
val result = converter.convert(available)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue