From df10b4fd21ccdb6ec72b6e01ff708fcdc87ad3b0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 May 2026 18:40:51 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../test/data/staking/MockYieldDTOFactory.kt | 2 +- .../models/response/model/YieldDTO.kt | 2 + core/res/src/main/res/values-ru/strings.xml | 6 + core/res/src/main/res/values/strings.xml | 29 +++ .../com/tangem/core/ui/utils/DateUtils.kt | 2 + .../data/staking/converters/YieldConverter.kt | 1 + .../domain/staking/model/stakekit/Yield.kt | 1 + .../domain/staking/model/CooldownPeriod.kt | 2 +- .../staking/model/P2PEthPoolIntegration.kt | 2 +- .../com/tangem/domain/staking/model/Period.kt | 14 ++ .../staking/model/StakeKitIntegration.kt | 18 +- .../staking/model/StakingIntegration.kt | 2 +- .../staking/model/StakeKitIntegrationTest.kt | 206 ++++++++++++++++ .../StakingBalanceEntryConverter.kt | 33 +-- .../SetInitialDataStateTransformer.kt | 28 ++- .../state/utils/CooldownPeriodUtils.kt | 28 ++- .../state/utils/CooldownPeriodUtilsTest.kt | 221 ++++++++++++++++++ 17 files changed, 559 insertions(+), 38 deletions(-) create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/model/Period.kt create mode 100644 domain/staking/src/test/kotlin/com/tangem/domain/staking/model/StakeKitIntegrationTest.kt create mode 100644 features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/state/utils/CooldownPeriodUtilsTest.kt diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt index 52d77fa3a1..9ad0a6c814 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt @@ -73,7 +73,7 @@ object MockYieldDTOFactory { type = "type", rewardSchedule = YieldDTO.MetadataDTO.RewardScheduleDTO.DAY, cooldownPeriod = null, - warmupPeriod = YieldDTO.MetadataDTO.PeriodDTO(1), + warmupPeriod = YieldDTO.MetadataDTO.PeriodDTO(1, seconds = null), rewardClaiming = YieldDTO.MetadataDTO.RewardClaimingDTO.AUTO, defaultValidator = null, minimumStake = null, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt index be0fb0cf91..67bae3eb18 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt @@ -150,6 +150,8 @@ data class YieldDTO( data class PeriodDTO( @Json(name = "days") val days: Int?, + @Json(name = "seconds") + val seconds: Int?, ) @JsonClass(generateAdapter = true) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index b8e2e9e0e4..5f7b583b7d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -326,6 +326,12 @@ Скрыть Удерживайте, чтобы %s час + + %d час + %d часa + %d часов + %d часа + %dч назад %dч назад diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 403712fc0c..fe8fe609f3 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -217,6 +217,7 @@ Access denied Account Accounts + %s failed Activate Add Add funds @@ -233,6 +234,8 @@ Apply Approval Approve + Approved + Approving Attention Available networks Backup @@ -315,6 +318,10 @@ Hide Hold to %s hour + + %d hour + %d hours + %dh ago %dh ago @@ -364,6 +371,8 @@ %1$s — %2$s Read more Receive + Received + Receiving Recommended Reject Reload @@ -382,6 +391,8 @@ Send Send: Failed to send transaction + Sending + Sent The server is not available, please try again later Share Share Link @@ -392,6 +403,7 @@ Skip Something went wrong Stake + Staked Staking Start Submit @@ -399,6 +411,8 @@ Support Supported networks Swap + Swapped + Swapping Tangem Tangem Wallet Tap and hold @@ -416,6 +430,7 @@ Transaction status Transactions Transfer + Transferred Unable to load data… I understand I understand, continue @@ -425,10 +440,12 @@ Unstake Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. Value copied + Voting Wallets Warning week with + Withdrawing Yes Yield Mode Contract address copied! @@ -590,6 +607,7 @@ Best rate FCA Warning List Fixed rate is unavailable + Provider for swap Competitive rate Provider in FCA warning list Available up to %s @@ -1334,6 +1352,8 @@ Target account is not created. Please change the amount to send. The amount to send must be at least %s Leave %s + A trustline for %s is required first. + Can\'t receive token Reduce by %s Reduce to %s Kindly be aware that your transaction may experience delays under specific fee settings @@ -1820,12 +1840,21 @@ Unavailable to sell Unavailable for swap from %s Unavailable for swap + Claiming reward contract: %s + Disabling Yield mode + Earned from stake You don\'t have any transactions yet Failed to load transaction history.\nClick on reload button to update the information. + from: %%image%% %s Multiple addresses Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer. Operation + Pending + Rewards restaked + Rewards restaking + Staking reward + to: %%image%% %s for: %s from: %s to: %s diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt index 6c5be9aecd..85378d4bc1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt @@ -8,6 +8,8 @@ import org.joda.time.DateTimeZone import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormatter +const val SECONDS_IN_HOUR = 3600 + /** * If [this] timestamp is today or yesterday, returns relative date, * otherwise returns formatting date. diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index 9766779759..08217059f4 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -117,6 +117,7 @@ internal object YieldConverter : Converter { private fun convertPeriod(periodDTO: YieldDTO.MetadataDTO.PeriodDTO): Yield.Metadata.Period { return Yield.Metadata.Period( days = periodDTO.days.asMandatory("days"), + seconds = periodDTO.seconds, ) } diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt index e1cf32955b..fc9daf4a8e 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt @@ -114,6 +114,7 @@ data class Yield( @Serializable data class Period( val days: Int, + val seconds: Int?, ) @Serializable diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/CooldownPeriod.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/CooldownPeriod.kt index bc66de0687..ea4ef686c0 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/CooldownPeriod.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/CooldownPeriod.kt @@ -2,7 +2,7 @@ package com.tangem.domain.staking.model sealed class CooldownPeriod { - data class Fixed(val days: Int) : CooldownPeriod() + data class Fixed(val period: Period) : CooldownPeriod() data class Range(val minDays: Int, val maxDays: Int) : CooldownPeriod() } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/P2PEthPoolIntegration.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/P2PEthPoolIntegration.kt index 7f779afa4e..cf61efdcff 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/P2PEthPoolIntegration.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/P2PEthPoolIntegration.kt @@ -62,7 +62,7 @@ class P2PEthPoolIntegration( // Metadata - override val warmupPeriodDays: Int = 0 + override val warmupPeriod: Period = Period.Days(0) override val cooldownPeriod: CooldownPeriod = CooldownPeriod.Range( minDays = MIN_COOLDOWN_DAYS, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/Period.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/Period.kt new file mode 100644 index 0000000000..e54d402e00 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/Period.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.staking.model + +sealed class Period { + + abstract val value: Int + + data class Days( + override val value: Int, + ) : Period() + + data class Seconds( + override val value: Int, + ) : Period() +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakeKitIntegration.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakeKitIntegration.kt index 185d52ea8b..8f1017dddb 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakeKitIntegration.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakeKitIntegration.kt @@ -49,10 +49,22 @@ class StakeKitIntegration( // Metadata - override val warmupPeriodDays: Int = yield.metadata.warmupPeriod.days + override val warmupPeriod: Period = yield.metadata.warmupPeriod.let { period -> + period.seconds?.let { + Period.Seconds(it) + } ?: period.days.let { + Period.Days(it) + } + } - override val cooldownPeriod: CooldownPeriod? = yield.metadata.cooldownPeriod?.days?.let { - CooldownPeriod.Fixed(it) + override val cooldownPeriod: CooldownPeriod? = yield.metadata.cooldownPeriod?.let { period -> + CooldownPeriod.Fixed( + period.seconds?.let { + Period.Seconds(it) + } ?: period.days.let { + Period.Days(it) + }, + ) } override val rewardSchedule: RewardSchedule = yield.metadata.rewardSchedule.toRewardSchedule() diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegration.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegration.kt index 12895e0629..8acc7d79ca 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegration.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegration.kt @@ -43,7 +43,7 @@ sealed interface StakingIntegration { // Metadata - val warmupPeriodDays: Int + val warmupPeriod: Period val cooldownPeriod: CooldownPeriod? diff --git a/domain/staking/src/test/kotlin/com/tangem/domain/staking/model/StakeKitIntegrationTest.kt b/domain/staking/src/test/kotlin/com/tangem/domain/staking/model/StakeKitIntegrationTest.kt new file mode 100644 index 0000000000..26f80a5941 --- /dev/null +++ b/domain/staking/src/test/kotlin/com/tangem/domain/staking/model/StakeKitIntegrationTest.kt @@ -0,0 +1,206 @@ +package com.tangem.domain.staking.model + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.staking.NetworkType +import com.tangem.domain.models.staking.YieldToken +import com.tangem.domain.staking.model.stakekit.AddressArgument +import com.tangem.domain.staking.model.stakekit.Yield +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.math.BigDecimal + +/** + * Tests for [StakeKitIntegration] — specifically the Period/CooldownPeriod mapping from [Yield.Metadata]. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class StakeKitIntegrationTest { + + // region helpers + + private val dummyToken = YieldToken( + name = "Test Token", + network = NetworkType.SOLANA, + symbol = "SOL", + decimals = 9, + address = null, + coinGeckoId = null, + logoURI = null, + isPoints = false, + ) + + private val dummyEnter = Yield.Args.Enter( + addresses = Yield.Args.Enter.Addresses( + address = AddressArgument(required = false), + ), + args = emptyMap(), + ) + + private val dummyArgs = Yield.Args(enter = dummyEnter, exit = null) + + private val dummyStatus = Yield.Status(enter = true, exit = null) + + private val dummyEnabled = Yield.Metadata.Enabled(enabled = true) + + private fun buildYield( + warmupPeriod: Yield.Metadata.Period, + cooldownPeriod: Yield.Metadata.Period?, + ): Yield { + return Yield( + id = "test-integration", + token = dummyToken, + tokens = emptyList(), + args = dummyArgs, + status = dummyStatus, + apy = BigDecimal("5.0"), + rewardRate = 5.0, + rewardType = com.tangem.domain.staking.model.common.RewardType.APY, + metadata = Yield.Metadata( + name = "Test Staking", + logoUri = "https://example.com/logo.png", + description = "Test staking integration", + documentation = null, + gasFeeToken = dummyToken, + token = dummyToken, + tokens = emptyList(), + type = "liquid", + rewardSchedule = Yield.Metadata.RewardSchedule.DAY, + cooldownPeriod = cooldownPeriod, + warmupPeriod = warmupPeriod, + rewardClaiming = Yield.Metadata.RewardClaiming.AUTO, + defaultValidator = null, + minimumStake = null, + supportsMultipleValidators = false, + revshare = dummyEnabled, + fee = dummyEnabled, + ), + validators = emptyList(), + isAvailable = true, + ) + } + + private fun buildIntegration( + warmupPeriod: Yield.Metadata.Period, + cooldownPeriod: Yield.Metadata.Period?, + ): StakeKitIntegration { + return StakeKitIntegration( + integrationId = StakingIntegrationID.StakeKit.Coin.Solana, + yield = buildYield(warmupPeriod = warmupPeriod, cooldownPeriod = cooldownPeriod), + ) + } + + // endregion + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class `warmupPeriod mapping` { + + @Test + fun `should produce Period Seconds when seconds is non-null`() { + // given + val warmup = Yield.Metadata.Period(days = 3, seconds = 7200) + + // when + val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null) + + // then + assertThat(integration.warmupPeriod).isEqualTo(Period.Seconds(7200)) + } + + @Test + fun `should produce Period Days when seconds is null`() { + // given + val warmup = Yield.Metadata.Period(days = 5, seconds = null) + + // when + val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null) + + // then + assertThat(integration.warmupPeriod).isEqualTo(Period.Days(5)) + } + + @Test + fun `should prefer seconds over days when both are present`() { + // given — days is non-zero but seconds takes priority + val warmup = Yield.Metadata.Period(days = 10, seconds = 3600) + + // when + val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null) + + // then + assertThat(integration.warmupPeriod).isInstanceOf(Period.Seconds::class.java) + assertThat((integration.warmupPeriod as Period.Seconds).value).isEqualTo(3600) + } + + @Test + fun `should produce Period Days with zero value when days is zero and seconds is null`() { + // given + val warmup = Yield.Metadata.Period(days = 0, seconds = null) + + // when + val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null) + + // then + assertThat(integration.warmupPeriod).isEqualTo(Period.Days(0)) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class `cooldownPeriod mapping` { + + @Test + fun `should be null when yield cooldownPeriod is null`() { + // given + val warmup = Yield.Metadata.Period(days = 1, seconds = null) + + // when + val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = null) + + // then + assertThat(integration.cooldownPeriod).isNull() + } + + @Test + fun `should produce Fixed Period Seconds when cooldown seconds is non-null`() { + // given + val warmup = Yield.Metadata.Period(days = 1, seconds = null) + val cooldown = Yield.Metadata.Period(days = 2, seconds = 86400) + + // when + val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = cooldown) + + // then + assertThat(integration.cooldownPeriod).isEqualTo(CooldownPeriod.Fixed(Period.Seconds(86400))) + } + + @Test + fun `should produce Fixed Period Days when cooldown seconds is null`() { + // given + val warmup = Yield.Metadata.Period(days = 1, seconds = null) + val cooldown = Yield.Metadata.Period(days = 3, seconds = null) + + // when + val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = cooldown) + + // then + assertThat(integration.cooldownPeriod).isEqualTo(CooldownPeriod.Fixed(Period.Days(3))) + } + + @Test + fun `should prefer seconds over days in cooldown when both are present`() { + // given + val warmup = Yield.Metadata.Period(days = 1, seconds = null) + val cooldown = Yield.Metadata.Period(days = 7, seconds = 604800) + + // when + val integration = buildIntegration(warmupPeriod = warmup, cooldownPeriod = cooldown) + + // then + val period = integration.cooldownPeriod + assertThat(period).isInstanceOf(CooldownPeriod.Fixed::class.java) + assertThat((period as CooldownPeriod.Fixed).period).isInstanceOf(Period.Seconds::class.java) + assertThat((period.period as Period.Seconds).value).isEqualTo(604800) + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/StakingBalanceEntryConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/StakingBalanceEntryConverter.kt index 2cee0bc82d..0417ba1bf3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/StakingBalanceEntryConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/StakingBalanceEntryConverter.kt @@ -1,23 +1,16 @@ package com.tangem.features.staking.impl.presentation.state.converters -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.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.utils.SECONDS_IN_HOUR import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.BalanceType -import com.tangem.domain.models.staking.PendingAction -import com.tangem.domain.models.staking.StakingBalance -import com.tangem.domain.models.staking.StakingBalanceEntry -import com.tangem.domain.models.staking.StakingEntryActions -import com.tangem.domain.models.staking.StakingEntryType +import com.tangem.domain.models.staking.* import com.tangem.domain.models.staking.action.StakingActionType +import com.tangem.domain.staking.model.Period import com.tangem.domain.staking.model.StakingIntegration import com.tangem.domain.staking.model.StakingTarget import com.tangem.features.staking.impl.R @@ -125,12 +118,26 @@ internal class StakingBalanceEntryConverter( } } StakingEntryType.PREPARING -> { - val warmupPeriod = integration.warmupPeriodDays + val warmupPeriod = integration.warmupPeriod TextReference.Combined( wrappedList( resourceReference(R.string.staking_details_warmup_period), stringReference(" "), - pluralReference(R.plurals.common_days, warmupPeriod, wrappedList(warmupPeriod)), + when (warmupPeriod) { + is Period.Days -> pluralReference( + id = R.plurals.common_days, + count = warmupPeriod.value, + formatArgs = wrappedList(warmupPeriod.value), + ) + is Period.Seconds -> { + val hours = warmupPeriod.value / SECONDS_IN_HOUR + pluralReference( + id = R.plurals.common_hours, + count = hours, + formatArgs = wrappedList(hours), + ) + } + }, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 97e0137e20..bb57a75f24 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -7,18 +7,20 @@ import com.tangem.common.ui.amountScreen.converters.AmountStateConverter import com.tangem.common.ui.amountScreen.models.AmountParameters import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.core.ui.utils.SECONDS_IN_HOUR import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.StakingBalanceEntry import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.model.Period import com.tangem.domain.staking.model.StakingIntegration import com.tangem.domain.staking.model.StakingTarget import com.tangem.domain.staking.model.common.RewardClaiming @@ -199,17 +201,27 @@ internal class SetInitialDataStateTransformer( } private fun createWarmupPeriodItem(): RoundedListWithDividersItemData? { - val warmupPeriodDays = integration.warmupPeriodDays - if (warmupPeriodDays == 0) return null + val warmupPeriod = integration.warmupPeriod + if (warmupPeriod.value == 0) return null return RoundedListWithDividersItemData( id = R.string.staking_details_warmup_period, startText = TextReference.Res(R.string.staking_details_warmup_period), - endText = pluralReference( - id = R.plurals.common_days, - count = warmupPeriodDays, - formatArgs = wrappedList(warmupPeriodDays), - ), + endText = when (warmupPeriod) { + is Period.Days -> pluralReference( + id = R.plurals.common_days, + count = warmupPeriod.value, + formatArgs = wrappedList(warmupPeriod.value), + ) + is Period.Seconds -> { + val hours = warmupPeriod.value / SECONDS_IN_HOUR + pluralReference( + id = R.plurals.common_hours, + count = hours, + formatArgs = wrappedList(hours), + ) + } + }, iconClick = { clickIntents.onInfoClick(InfoType.WARMUP_PERIOD) }, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/CooldownPeriodUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/CooldownPeriodUtils.kt index f31d5d3b6e..7237a3b641 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/CooldownPeriodUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/CooldownPeriodUtils.kt @@ -1,22 +1,30 @@ package com.tangem.features.staking.impl.presentation.state.utils -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.stringReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.utils.SECONDS_IN_HOUR import com.tangem.domain.staking.model.CooldownPeriod +import com.tangem.domain.staking.model.Period import com.tangem.features.staking.impl.R import com.tangem.utils.StringsSigns.MINUS import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE internal fun CooldownPeriod.toTextReference(): TextReference { return when (this) { - is CooldownPeriod.Fixed -> pluralReference( - id = R.plurals.common_days, - count = days, - formatArgs = wrappedList(days), - ) + is CooldownPeriod.Fixed -> when (period) { + is Period.Days -> pluralReference( + id = R.plurals.common_days, + count = period.value, + formatArgs = wrappedList(period.value), + ) + is Period.Seconds -> { + val hours = period.value / SECONDS_IN_HOUR + pluralReference( + id = R.plurals.common_hours, + count = hours, + formatArgs = wrappedList(hours), + ) + } + } is CooldownPeriod.Range -> combinedReference( stringReference("$minDays$MINUS$maxDays$NON_BREAKING_SPACE"), pluralReference( diff --git a/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/state/utils/CooldownPeriodUtilsTest.kt b/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/state/utils/CooldownPeriodUtilsTest.kt new file mode 100644 index 0000000000..2c60ad2b9a --- /dev/null +++ b/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/state/utils/CooldownPeriodUtilsTest.kt @@ -0,0 +1,221 @@ +package com.tangem.features.staking.impl.presentation.state.utils + +import com.google.common.truth.Truth.assertThat +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.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.SECONDS_IN_HOUR +import com.tangem.domain.staking.model.CooldownPeriod +import com.tangem.domain.staking.model.Period +import com.tangem.features.staking.impl.R +import com.tangem.utils.StringsSigns.MINUS +import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** + * Tests for [CooldownPeriod.toTextReference] extension function. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class CooldownPeriodUtilsTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class `Fixed with Days` { + + @Test + fun `should return plural days reference for Fixed Period Days`() { + // given + val cooldown = CooldownPeriod.Fixed(Period.Days(7)) + + // when + val result = cooldown.toTextReference() + + // then + val expected = pluralReference( + id = R.plurals.common_days, + count = 7, + formatArgs = wrappedList(7), + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `should return plural days with zero for Fixed Period Days zero`() { + // given + val cooldown = CooldownPeriod.Fixed(Period.Days(0)) + + // when + val result = cooldown.toTextReference() + + // then + val expected = pluralReference( + id = R.plurals.common_days, + count = 0, + formatArgs = wrappedList(0), + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `should pass day count as both count and format arg`() { + // given + val days = 14 + val cooldown = CooldownPeriod.Fixed(Period.Days(days)) + + // when + val result = cooldown.toTextReference() + + // then + assertThat(result).isInstanceOf(TextReference.PluralRes::class.java) + val plural = result as TextReference.PluralRes + assertThat(plural.count).isEqualTo(days) + assertThat(plural.formatArgs.first()).isEqualTo(days) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class `Fixed with Seconds` { + + @Test + fun `should return plural hours reference for Fixed Period Seconds`() { + // given — 2 hours worth of seconds + val cooldown = CooldownPeriod.Fixed(Period.Seconds(2 * SECONDS_IN_HOUR)) + + // when + val result = cooldown.toTextReference() + + // then + val expectedHours = 2 + val expected = pluralReference( + id = R.plurals.common_hours, + count = expectedHours, + formatArgs = wrappedList(expectedHours), + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `should convert exactly one hour worth of seconds to 1 hour`() { + // given + val cooldown = CooldownPeriod.Fixed(Period.Seconds(SECONDS_IN_HOUR)) + + // when + val result = cooldown.toTextReference() + + // then + assertThat(result).isInstanceOf(TextReference.PluralRes::class.java) + val plural = result as TextReference.PluralRes + assertThat(plural.count).isEqualTo(1) + assertThat(plural.formatArgs.first()).isEqualTo(1) + } + + @Test + fun `should floor to 1 hour when seconds are not divisible evenly`() { + // given — 5000 seconds = 1.388... hours → integer division → 1 + val cooldown = CooldownPeriod.Fixed(Period.Seconds(5000)) + + // when + val result = cooldown.toTextReference() + + // then + assertThat(result).isInstanceOf(TextReference.PluralRes::class.java) + val plural = result as TextReference.PluralRes + assertThat(plural.count).isEqualTo(1) + } + + @Test + fun `should return 0 hours for zero seconds`() { + // given + val cooldown = CooldownPeriod.Fixed(Period.Seconds(0)) + + // when + val result = cooldown.toTextReference() + + // then + assertThat(result).isInstanceOf(TextReference.PluralRes::class.java) + val plural = result as TextReference.PluralRes + assertThat(plural.count).isEqualTo(0) + assertThat(plural.formatArgs.first()).isEqualTo(0) + } + + @Test + fun `should use R plurals common_hours resource id`() { + // given + val cooldown = CooldownPeriod.Fixed(Period.Seconds(SECONDS_IN_HOUR)) + + // when + val result = cooldown.toTextReference() + + // then + assertThat(result).isInstanceOf(TextReference.PluralRes::class.java) + assertThat((result as TextReference.PluralRes).id).isEqualTo(R.plurals.common_hours) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Range { + + @Test + fun `should return combined reference for Range`() { + // given + val minDays = 2 + val maxDays = 5 + val cooldown = CooldownPeriod.Range(minDays = minDays, maxDays = maxDays) + + // when + val result = cooldown.toTextReference() + + // then + val expected = combinedReference( + stringReference("$minDays$MINUS$maxDays$NON_BREAKING_SPACE"), + pluralReference( + id = R.plurals.common_days_no_param, + count = maxDays, + ), + ) + assertThat(result).isEqualTo(expected) + } + + @Test + fun `should use maxDays as count for plural in Range`() { + // given + val maxDays = 10 + val cooldown = CooldownPeriod.Range(minDays = 3, maxDays = maxDays) + + // when + val result = cooldown.toTextReference() + + // then + assertThat(result).isInstanceOf(TextReference.Combined::class.java) + val combined = result as TextReference.Combined + val pluralPart = combined.refs[1] + assertThat(pluralPart).isInstanceOf(TextReference.PluralRes::class.java) + assertThat((pluralPart as TextReference.PluralRes).count).isEqualTo(maxDays) + } + + @Test + fun `should include minDays and maxDays with minus and non-breaking-space in string part`() { + // given + val minDays = 1 + val maxDays = 7 + val cooldown = CooldownPeriod.Range(minDays = minDays, maxDays = maxDays) + + // when + val result = cooldown.toTextReference() + + // then + assertThat(result).isInstanceOf(TextReference.Combined::class.java) + val combined = result as TextReference.Combined + val stringPart = combined.refs[0] + assertThat(stringPart).isInstanceOf(TextReference.Str::class.java) + assertThat((stringPart as TextReference.Str).value) + .isEqualTo("$minDays$MINUS$maxDays$NON_BREAKING_SPACE") + } + } +} \ No newline at end of file