Updated on 2026-08-14
This commit is contained in:
parent
ff8f9c9b18
commit
6aab25427d
75 changed files with 2122 additions and 57 deletions
|
|
@ -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,15 +63,29 @@ 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(),
|
||||
) {
|
||||
CurrentApy(state.apy)
|
||||
chartComponent.Content(Modifier)
|
||||
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()) {
|
||||
|
|
@ -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(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
apy = apy,
|
||||
),
|
||||
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,
|
||||
),
|
||||
stringReference(" ${tokenStatus.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,21 +65,91 @@ internal fun YieldSupplyBlockContentLegacy(yieldSupplyUM: YieldSupplyUM, modifie
|
|||
|
||||
@Composable
|
||||
private fun SupplyAvailable(supplyUM: YieldSupplyUM.Available, modifier: Modifier = Modifier) {
|
||||
SupplyInfo(
|
||||
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,
|
||||
modifier = modifier,
|
||||
button = {
|
||||
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,
|
||||
),
|
||||
subtitle = resourceReference(R.string.yield_module_token_details_earn_notification_description),
|
||||
rewardsApy = supplyUM.apyText,
|
||||
iconTint = TangemTheme.colors.icon.accent,
|
||||
modifier = modifier,
|
||||
button = {
|
||||
SecondaryButton(
|
||||
text = stringResourceSafe(R.string.common_learn_more),
|
||||
onClick = supplyUM.onClick,
|
||||
size = TangemButtonSize.WideAction,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SupplyAvailableBoosted(supplyUM: YieldSupplyUM.Available, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_analytics_up_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f), CircleShape)
|
||||
.padding(6.dp)
|
||||
.size(24.dp),
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
Text(
|
||||
text = supplyUM.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = supplyUM.apyText.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.yield_apy_boost_banner_subtitle),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
SecondaryButton(
|
||||
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.fillMaxWidth(),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -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)
|
||||
Text(
|
||||
text = yieldSupplyPromoUM.title.resolveReference(),
|
||||
style = TangemTheme.typography.h2,
|
||||
textAlign = TextAlign.Center,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
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