diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 532e167dbd..790bb01a7a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -4,6 +4,7 @@ import com.tangem.domain.settings.* import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase +import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.repositories.StakingRepository import dagger.Module import dagger.Provides @@ -31,6 +32,14 @@ internal object StakingDomainModule { ) } + @Provides + @Singleton + fun provideGetYieldUseCase(stakingRepository: StakingRepository): GetYieldUseCase { + return GetYieldUseCase( + stakingRepository = stakingRepository, + ) + } + @Provides @Singleton fun provideGetStakingTokensUseCase(stakingRepository: StakingRepository): FetchStakingTokensUseCase { diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 07650632f0..c900ea3b50 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -20,6 +20,7 @@ import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.send.api.featuretoggles.SendFeatureToggles +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.tap.common.extensions.setContext import com.tangem.tap.features.main.model.MainScreenState import com.tangem.tap.store @@ -45,6 +46,7 @@ internal class MainViewModel @Inject constructor( private val sendFeatureToggles: SendFeatureToggles, private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, + private val stakingFeatureToggles: StakingFeatureToggles, private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -71,7 +73,10 @@ internal class MainViewModel @Inject constructor( observeFlips() displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() - fetchStakingTokens() + + if (stakingFeatureToggles.isStakingEnabled) { + fetchStakingTokens() + } viewModelScope.launch(dispatchers.main) { deleteDeprecatedLogsUseCase() 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 3e0c439e09..bf8956f11f 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 @@ -75,7 +75,7 @@ data class YieldDTO( @Json(name = "website") val website: String?, @Json(name = "apr") - val apr: Double?, + val apr: BigDecimal?, @Json(name = "commission") val commission: Double?, @Json(name = "stakedBalance") diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 67531e72a0..afda2fa213 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1,5 +1,6 @@ + Выберите сеть Добавить токен Валюты Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. @@ -300,6 +301,7 @@ %1$d из %2$d кошельков например Bitcoin + Ваш портфель был обновлен Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. Голосовать Выберите кошелек diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 130168cb3a..8ef0f52ec8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ + Choose network Add custom token Manage tokens Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. @@ -64,6 +65,7 @@ Not enough ADA Accept Access denied + Allow Apply Approval Attention @@ -111,6 +113,7 @@ Primary Card Passphrase Paste + %1$s-%2$s Read more Receive Reject @@ -300,10 +303,13 @@ %1$d of %2$d wallets e.g. BTC I trust, hodl I must + Your portfolio has been updated The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. Upvote Choose wallet The wallet doesn\'t support more than one network + Market + Sort By You have to set up a single access code to protect all your cards Protect You can set up an individual access code on each card later @@ -536,9 +542,13 @@ Forget wallet This will remove the wallet from the application. The wallet itself can be added again. Name + APR + APY Available Average Reward Rate %s est. profit + Market rating + Metrics Minimum Requirement No rewards to claim On stake @@ -627,6 +637,9 @@ This action is irreversible. You will not have access to the old wallet. Tap the twin card with number %s and do not remove until the end of the operation Use %s or scan a card to have an access to your wallet + Stay up to date with the latest features and news + Be able to get important market notifications + Would you like to use
Push-notifications? Add new wallet Are you sure you want to delete this wallet? An error has occurred, please scan your card to log in diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt new file mode 100644 index 0000000000..e83444c298 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt @@ -0,0 +1,147 @@ +package com.tangem.core.ui.components.rows + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +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.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.R + +@Suppress("LongParameterList") +@Composable +fun RoundableCornersRow( + startText: String, + startTextColor: Color, + startTextStyle: TextStyle, + endText: String, + endTextColor: Color, + endTextStyle: TextStyle, + cornersToRound: CornersToRound, + iconResId: Int? = null, +) { + Surface( + shape = cornersToRound.getShape(), + color = TangemTheme.colors.background.primary, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(TangemTheme.dimens.size48) + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = startText, + color = startTextColor, + maxLines = 1, + style = startTextStyle, + ) + if (iconResId != null) { + Icon( + modifier = Modifier + .padding(TangemTheme.dimens.spacing4) + .size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_alert_24), + contentDescription = null, + tint = TangemTheme.colors.text.tertiary, + ) + } + Spacer(modifier = Modifier.weight(1f)) + Text( + text = endText, + color = endTextColor, + maxLines = 1, + style = endTextStyle, + ) + } + } +} + +enum class CornersToRound { + + ALL_4, + TOP_2, + BOTTOM_2, + ZERO, + ; + + @Suppress("TopLevelComposableFunctions") + @Composable + fun getShape(): RoundedCornerShape { + val radius = TangemTheme.dimens.radius12 + return when (this) { + ALL_4 -> RoundedCornerShape(radius) + TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius) + BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) + ZERO -> RoundedCornerShape(0.dp) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_RoundableCornersRow( + @PreviewParameter(RoundableCornersRowDataProvider::class) previewData: RoundableCornersRowPreviewData, +) { + TangemThemePreview { + Box(modifier = Modifier.background(color = TangemTheme.colors.icon.attention)) { + RoundableCornersRow( + startText = previewData.startText, + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = previewData.endText, + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.subtitle2, + cornersToRound = previewData.cornersToRound, + iconResId = previewData.iconResId, + ) + } + } +} + +private data class RoundableCornersRowPreviewData( + val startText: String, + val endText: String, + val cornersToRound: CornersToRound, + val iconResId: Int? = null, +) + +private class RoundableCornersRowDataProvider : + PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + getPreviewData(cornersToRound = CornersToRound.ZERO), + getPreviewData(cornersToRound = CornersToRound.TOP_2), + getPreviewData(cornersToRound = CornersToRound.BOTTOM_2), + getPreviewData(cornersToRound = CornersToRound.ZERO, iconResId = R.drawable.ic_alert_24), + getPreviewData(cornersToRound = CornersToRound.TOP_2, iconResId = R.drawable.ic_alert_24), + getPreviewData(cornersToRound = CornersToRound.BOTTOM_2, iconResId = R.drawable.ic_alert_24), + ) + + private fun getPreviewData(cornersToRound: CornersToRound, iconResId: Int? = null) = RoundableCornersRowPreviewData( + startText = "startText", + endText = "endText", + cornersToRound = cornersToRound, + iconResId = iconResId, + ) +} \ No newline at end of file diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 3c16b7c878..186d53da6e 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -16,7 +16,6 @@ dependencies { implementation(projects.core.utils) implementation(projects.domain.tokens.models) implementation(projects.domain.staking) - implementation(projects.features.staking.api) // region DI diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index be421d5a75..cc0d2ba7b3 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -11,28 +11,62 @@ import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.Yield import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, - private val stakingFeatureToggles: StakingFeatureToggles, private val stakingYieldsStore: StakingYieldsStore, private val dispatchers: CoroutineDispatcherProvider, ) : StakingRepository { private val yieldConverter = YieldConverter() + override fun isStakingSupported(currencyId: String): Boolean { + return integrationIds.contains(currencyId) + } + + override suspend fun fetchEnabledYields() { + withContext(dispatchers.io) { + val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow() + + stakingYieldsStore.store(stakingTokensWithYields.data) + } + } + + override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield { + return withContext(dispatchers.io) { + val yields = getEnabledYields() ?: error("No yields found") + val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: error("Staking custom tokens is not available") + + val prefetchedYield = findPrefetchedYield( + yields = yields, + currencyId = rawCurrencyId, + symbol = symbol, + ) + + prefetchedYield ?: error("Staking is unavailable") + } + } + + override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo { + return withContext(dispatchers.io) { + val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow() + + StakingEntryInfo( + interestRate = yield.apy, + periodInDays = yield.metadata.cooldownPeriod.days, + tokenSymbol = yield.token.symbol, + ) + } + } + override suspend fun getStakingAvailabilityForActions( cryptoCurrencyId: CryptoCurrency.ID, symbol: String, ): StakingAvailability { val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: return StakingAvailability.Unavailable - if (!stakingFeatureToggles.isStakingEnabled) { - return StakingAvailability.Unavailable - } return withContext(dispatchers.io) { val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable @@ -56,34 +90,6 @@ internal class DefaultStakingRepository( .find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } } - override fun isStakingSupported(currencyId: String): Boolean { - return integrationIds.contains(currencyId) - } - - override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo { - return withContext(dispatchers.io) { - val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow() - - StakingEntryInfo( - interestRate = yield.apy, - periodInDays = yield.metadata.cooldownPeriod.days, - tokenSymbol = yield.token.symbol, - ) - } - } - - override suspend fun fetchEnabledYields() { - if (!stakingFeatureToggles.isStakingEnabled) { - return - } - - withContext(dispatchers.io) { - val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow() - - stakingYieldsStore.store(stakingTokensWithYields.data) - } - } - private suspend fun getEnabledYields(): List? { val yields = stakingYieldsStore.getSyncOrNull() ?: return null return yields.map { yieldConverter.convert(it) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 092e8cfc62..0b47b925d1 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -4,7 +4,6 @@ import com.tangem.data.staking.DefaultStakingRepository import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -20,13 +19,11 @@ internal object StakingDataModule { @Singleton fun provideStakingRepository( stakeKitApi: StakeKitApi, - stakingFeatureToggles: StakingFeatureToggles, stakingTokenStore: StakingYieldsStore, dispatchers: CoroutineDispatcherProvider, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, - stakingFeatureToggles = stakingFeatureToggles, stakingYieldsStore = stakingTokenStore, dispatchers = dispatchers, ) diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index 80ff7080b9..252bfb49d9 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -12,5 +12,8 @@ android { dependencies { implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) + implementation(deps.kotlin.serialization) + + implementation(projects.domain.tokens.models) } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt new file mode 100644 index 0000000000..a63dc81cdc --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency + +/** + * Use case for getting staking yield for staking scenario start. + */ +class GetYieldUseCase(private val stakingRepository: StakingRepository) { + + suspend operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Either { + return Either.catch { stakingRepository.getYield(cryptoCurrencyId, symbol) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt index 3ef85e77dd..853c3d4247 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt @@ -1,5 +1,6 @@ package com.tangem.domain.staking.model +import java.io.Serializable import java.math.BigDecimal data class Yield( @@ -14,24 +15,24 @@ data class Yield( val metadata: Metadata, val validators: List, val isAvailable: Boolean, -) { +) : Serializable { data class Status( val enter: Boolean, val exit: Boolean?, - ) + ) : Serializable data class Args( val enter: Enter, val exit: Enter?, - ) { + ) : Serializable { data class Enter( val addresses: Addresses, val args: Map, - ) { + ) : Serializable { data class Addresses( val address: AddressArgument, val additionalAddresses: Map? = null, - ) + ) : Serializable } } @@ -41,12 +42,12 @@ data class Yield( val name: String, val image: String?, val website: String?, - val apr: Double?, + val apr: BigDecimal?, val commission: Double?, val stakedBalance: String?, val votingPower: Double?, val preferred: Boolean, - ) + ) : Serializable data class Metadata( val name: String, @@ -66,14 +67,14 @@ data class Yield( val supportsMultipleValidators: Boolean, val revshare: Enabled, val fee: Enabled, - ) { + ) : Serializable { data class Period( val days: Int, - ) + ) : Serializable data class Enabled( val enabled: Boolean, - ) + ) : Serializable } enum class RewardType { @@ -92,7 +93,7 @@ data class Token( val coinGeckoId: String?, val logoURI: String?, val isPoints: Boolean?, -) { +) : Serializable { enum class NetworkType { AVALANCHE_C, AVALANCHE_ATOMIC, @@ -169,4 +170,4 @@ data class AddressArgument( val network: String? = null, val minimum: Double? = null, val maximum: Double? = null, -) \ No newline at end of file +) : Serializable \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index eafbb514fb..2e1fe82f5d 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -2,18 +2,21 @@ package com.tangem.domain.staking.repositories import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.model.CryptoCurrency interface StakingRepository { + fun isStakingSupported(currencyId: String): Boolean + + suspend fun fetchEnabledYields() + + suspend fun getEntryInfo(integrationId: String): StakingEntryInfo + + suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield + suspend fun getStakingAvailabilityForActions( cryptoCurrencyId: CryptoCurrency.ID, symbol: String, ): StakingAvailability - - suspend fun getEntryInfo(integrationId: String): StakingEntryInfo - - suspend fun fetchEnabledYields() - - fun isStakingSupported(currencyId: String): Boolean } \ No newline at end of file diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 0a90fdc329..900ab7de1b 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -12,12 +12,12 @@ android { dependencies { /** Core modules */ - implementation(project(":core:analytics")) + implementation(projects.core.analytics) implementation(projects.core.analytics.models) - implementation(project(":core:res")) - implementation(project(":core:utils")) - implementation(project(":core:ui")) - implementation(project(":libs:crypto")) + implementation(projects.core.res) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.libs.crypto) /** AndroidX */ implementation(deps.androidx.appCompat) @@ -30,7 +30,7 @@ dependencies { implementation(deps.compose.ui.tooling) /** Domain */ - implementation(project(":features:referral:domain")) + implementation(projects.features.referral.domain) /** Other libraries */ implementation(deps.compose.shimmer) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt deleted file mode 100644 index 5f11c074e1..0000000000 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.tangem.feature.referral.ui - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -@Suppress("LongParameterList") -@Composable -internal fun AwardText( - startText: String, - startTextColor: Color, - startTextStyle: TextStyle, - endText: String, - endTextColor: Color, - endTextStyle: TextStyle, - cornersToRound: CornersToRound, -) { - Surface( - shape = cornersToRound.getShape(), - color = TangemTheme.colors.background.primary, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(TangemTheme.dimens.size48) - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing12, - ), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = startText, - color = startTextColor, - maxLines = 1, - style = startTextStyle, - ) - - Text( - text = endText, - color = endTextColor, - maxLines = 1, - style = endTextStyle, - ) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_AwardItem() { - TangemThemePreview { - AwardText( - startText = "startText", - startTextColor = TangemTheme.colors.text.tertiary, - startTextStyle = TangemTheme.typography.subtitle2, - endText = "endText", - endTextColor = TangemTheme.colors.text.primary1, - endTextStyle = TangemTheme.typography.subtitle2, - cornersToRound = CornersToRound.TOP_2, - ) - } -} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt deleted file mode 100644 index b5070cdb28..0000000000 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.feature.referral.ui - -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemTheme - -internal enum class CornersToRound { - - ALL_4, - TOP_2, - BOTTOM_2, - ZERO, - ; - - @Suppress("TopLevelComposableFunctions") - @Composable - fun getShape(): RoundedCornerShape { - val radius = TangemTheme.dimens.radius12 - return when (this) { - ALL_4 -> RoundedCornerShape(radius) - TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius) - BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) - ZERO -> RoundedCornerShape(0.dp) - } - } -} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index cd403043df..01e2c4b3d1 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.core.content.ContextCompat.startActivity import com.tangem.core.ui.components.PrimaryButtonIconStart +import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.components.rows.RoundableCornersRow import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.domain.models.ExpectedAward @@ -82,7 +84,7 @@ private fun CounterAndAwards(purchasedWalletCount: Int, expectedAwards: Expected private fun Counter(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) { val isExpectedAwardsPresent = expectedAwards != null - AwardText( + RoundableCornersRow( startText = stringResource(id = R.string.referral_friends_bought_title), startTextColor = TangemTheme.colors.text.tertiary, startTextStyle = TangemTheme.typography.subtitle2, @@ -111,7 +113,7 @@ private fun Awards(expectedAwards: ExpectedAwards) { thickness = TangemTheme.dimens.size0_5, color = TangemTheme.colors.stroke.primary, ) - AwardText( + RoundableCornersRow( startText = if (expectedAwards.expectedAwards.isNotEmpty()) { stringResource(id = R.string.referral_expected_awards) } else { @@ -137,7 +139,7 @@ private fun Awards(expectedAwards: ExpectedAwards) { val extraItems = expectedAwards.expectedAwards.drop(elementsCountToShowInLessMode) initialItems.forEachIndexed { index, expectedAward -> - AwardText( + RoundableCornersRow( startText = expectedAward.paymentDate, startTextColor = TangemTheme.colors.text.primary1, startTextStyle = TangemTheme.typography.subtitle2, @@ -218,7 +220,7 @@ private fun LessMoreButton(isExpanded: MutableState) { private fun ExtraItems(extraItems: List) { Column { extraItems.forEach { expectedAward -> - AwardText( + RoundableCornersRow( startText = expectedAward.paymentDate, startTextColor = TangemTheme.colors.text.primary1, startTextStyle = TangemTheme.typography.subtitle2, diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt index 1749258dfd..05277770a8 100644 --- a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt +++ b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt @@ -7,7 +7,8 @@ interface StakingRouter { fun getEntryFragment(): Fragment companion object { - const val CRYPTO_CURRENCY_KEY = "CRYPTO_CURRENCY_KEY" + const val CRYPTO_CURRENCY_ID_KEY = "CRYPTO_CURRENCY_ID_KEY" const val USER_WALLET_ID_KEY = "USER_WALLET_ID_KEY" + const val YIELD_KEY = "YIELD_KEY" } } \ No newline at end of file diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 5640ea3503..e608669082 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.staking) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt index fab926ce46..e96be559de 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt @@ -13,6 +13,7 @@ import com.tangem.core.ui.screen.ComposeFragment import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.StakingStateRouter +import com.tangem.features.staking.impl.presentation.ui.StakingScreen import com.tangem.features.staking.impl.presentation.viewmodel.StakingViewModel import dagger.hilt.android.AndroidEntryPoint import java.lang.ref.WeakReference @@ -57,8 +58,8 @@ internal class StakingFragment : ComposeFragment() { SystemBarsEffect { setSystemBarsColor(systemBarsColor) } - val currentState = viewModel.stakingStateRouter.currentState.collectAsStateWithLifecycle() - // StakingScreen(viewModel.uiState, currentState.value) + val currentState = viewModel.uiState.collectAsStateWithLifecycle() + StakingScreen(currentState.value) } override fun onDestroy() { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 1d0095389b..9fdd84f082 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -1,8 +1,8 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.core.ui.event.consumedEvent +import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.StakingScreenStateTransformer -import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -33,7 +33,7 @@ internal class StakingStateController @Inject constructor() { private fun getInitialState(): StakingUiState { return StakingUiState( - clickIntents = object : StakingClickIntents {}, + clickIntents = StakingClickIntentsStub, cryptoCurrencyName = "", currentScreen = StakingUiStateType.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 8355cab544..3852cd6ed2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -2,6 +2,7 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents /** @@ -41,6 +42,14 @@ internal sealed class StakingStates { sealed class InitialInfoState : StakingStates() { data class Data( override val isPrimaryButtonEnabled: Boolean, + val available: String, + val onStake: String, + val aprRange: TextReference, + val unbondingPeriod: String, + val minimumRequirement: String, + val rewardClaiming: String, + val warmupPeriod: String, + val rewardSchedule: String, ) : InitialInfoState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt new file mode 100644 index 0000000000..16a8763435 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -0,0 +1,8 @@ +package com.tangem.features.staking.impl.presentation.state.stub + +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents + +object StakingClickIntentsStub : StakingClickIntents { + + override fun onBackClick() {} +} \ No newline at end of file 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 new file mode 100644 index 0000000000..3ee1b970cd --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -0,0 +1,77 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.core.ui.extensions.TextReference +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.utils.BigDecimalFormatter +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.StakingUiStateType +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.utils.Provider +import java.math.BigDecimal + +internal class SetInitialDataStateTransformer( + private val clickIntents: StakingClickIntents, + private val yield: Yield, + private val cryptoCurrencyStatusProvider: Provider, +) : StakingScreenStateTransformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + clickIntents = clickIntents, + currentScreen = StakingUiStateType.InitialInfo, + initialInfoState = createInitialInfoState(), + ) + } + + private fun createInitialInfoState(): StakingStates.InitialInfoState.Data { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + + return StakingStates.InitialInfoState.Data( + isPrimaryButtonEnabled = true, + available = BigDecimalFormatter.formatCryptoAmount( + cryptoCurrencyStatus.value.amount, + cryptoCurrency = cryptoCurrencyStatus.currency.symbol, + decimals = cryptoCurrencyStatus.currency.decimals, + ), + onStake = "0 SOL", // TODO staking add after adding /balances request + aprRange = getAprRange(), + unbondingPeriod = yield.metadata.cooldownPeriod.days.toString(), + minimumRequirement = yield.metadata.minimumStake.toString(), + rewardClaiming = yield.metadata.rewardClaiming, + warmupPeriod = yield.metadata.warmupPeriod.days.toString(), + rewardSchedule = yield.metadata.rewardSchedule, + + ) + } + + private fun getAprRange(): TextReference { + val aprValues = yield.validators.mapNotNull { it.apr } + + val minApr = aprValues.min() + val maxApr = aprValues.max() + + val formattedMinApr = BigDecimalFormatter.formatPercent( + percent = minApr, + useAbsoluteValue = true, + ) + val formattedMaxApr = BigDecimalFormatter.formatPercent( + percent = maxApr, + useAbsoluteValue = true, + ) + + if (maxApr - minApr < EQUALITY_THRESHOLD) { + return stringReference("$formattedMinApr%") + } + return resourceReference(R.string.common_percent_range, wrappedList(formattedMinApr, formattedMaxApr)) + } + + companion object { + private val EQUALITY_THRESHOLD = BigDecimal(1E-10) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt new file mode 100644 index 0000000000..7ff32c89e2 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -0,0 +1,200 @@ +package com.tangem.features.staking.impl.presentation.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +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.rows.CornersToRound +import com.tangem.core.ui.components.rows.RoundableCornersRow +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingStates + +@Composable +internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) { + if (state !is StakingStates.InitialInfoState.Data) return + + Column( + modifier = Modifier // Do not put fillMaxSize() in here + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) { + MetricsBlock(state) + Spacer(modifier = Modifier.height(8.dp)) + StakingDetailsRows(state) + Spacer(modifier = Modifier.height(8.dp)) + PrimaryButton( + text = stringResource(id = R.string.common_stake), + modifier = Modifier.fillMaxWidth(), + onClick = { + // TODO staking + }, + ) + } +} + +@Composable +private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) { + Column( + modifier = Modifier + .background( + color = TangemTheme.colors.background.primary, + shape = RoundedCornerShape(TangemTheme.dimens.radius12), + ) + .padding(16.dp) + .fillMaxWidth(), + ) { + Text( + text = stringResource(id = R.string.staking_details_metrics_block_header), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column(modifier = Modifier.weight(1F)) { + Text( + text = stringResource(id = R.string.staking_details_apr), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + text = state.aprRange.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.accent, + ) + } + Column(modifier = Modifier.weight(1F)) { + Row { + Text( + modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), + text = stringResource(id = R.string.staking_details_market_rating), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + modifier = Modifier + .size(TangemTheme.dimens.size16) + .align(Alignment.CenterVertically), + painter = painterResource(id = R.drawable.ic_alert_24), + contentDescription = null, + tint = TangemTheme.colors.text.tertiary, + ) + } + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + text = "1", // TODO staking + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.accent, + ) + } + } + } +} + +@Composable +internal fun StakingDetailsRows(state: StakingStates.InitialInfoState.Data) { + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_available), + endText = state.available, + cornersToRound = CornersToRound.TOP_2, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_on_stake), + endText = state.onStake, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_apy), + endText = state.aprRange.resolveReference(), + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_unbonding_period), + endText = state.unbondingPeriod, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_minimum_requirement), + endText = state.minimumRequirement, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_reward_claiming), + endText = state.rewardClaiming, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_warmup_period), + endText = state.warmupPeriod, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_reward_schedule), + endText = state.rewardSchedule, + cornersToRound = CornersToRound.BOTTOM_2, + ) +} + +@Composable +private fun InitialInfoContentRow(startText: String, endText: String, cornersToRound: CornersToRound) { + RoundableCornersRow( + startText = startText, + startTextColor = TangemTheme.colors.text.primary1, + startTextStyle = TangemTheme.typography.body2, + endText = endText, + endTextColor = TangemTheme.colors.text.tertiary, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = cornersToRound, + iconResId = null, // TODO staking add bottom sheets when text will be available + ) +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StakingInitialInfoContent_Preview( + @PreviewParameter(StakingInitialInfoContentPreviewProvider::class) feeState: StakingStates.InitialInfoState.Data, +) { + TangemThemePreview { + StakingInitialInfoContent( + state = feeState, + ) + } +} + +private class StakingInitialInfoContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + StakingStates.InitialInfoState.Data( + isPrimaryButtonEnabled = false, + available = "15 SOL", + onStake = "0 SOL", + aprRange = stringReference("2.54-5.12%"), + unbondingPeriod = "3d", + minimumRequirement = "12 SOL", + rewardClaiming = "Auto", + warmupPeriod = "Days", + rewardSchedule = "Block", + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt new file mode 100644 index 0000000000..c1f1bf5a95 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -0,0 +1,125 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.StakingUiStateType +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.withIndex + +@Composable +internal fun StakingScreen(uiState: StakingUiState) { + BackHandler(onBack = uiState.clickIntents::onBackClick) + Column( + modifier = Modifier + .fillMaxSize() + .imePadding() + .systemBarsPadding() + .background(color = TangemTheme.colors.background.tertiary), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SendAppBar( + uiState = uiState, + ) + StakingScreenContent( + uiState = uiState, + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun SendAppBar(uiState: StakingUiState) { + val titleRes = when (uiState.currentScreen) { + StakingUiStateType.InitialInfo -> stringResource(id = R.string.common_stake) + StakingUiStateType.Amount -> stringResource(id = R.string.send_amount_label) + StakingUiStateType.ValidatorAndFee -> stringResource(id = R.string.common_stake) + StakingUiStateType.Confirm -> "" + } + val backIcon = when (uiState.currentScreen) { + StakingUiStateType.Amount, + StakingUiStateType.ValidatorAndFee, + StakingUiStateType.Confirm, + -> { + R.drawable.ic_close_24 + } + StakingUiStateType.InitialInfo -> { + R.drawable.ic_back_24 + } + } + AppBarWithBackButtonAndIcon( + text = titleRes, + backIconRes = backIcon, + onBackClick = uiState.clickIntents::onBackClick, + backgroundColor = TangemTheme.colors.background.tertiary, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) { + val currentScreen = uiState.currentScreen + var currentStateProxy by remember { mutableStateOf(currentScreen) } + var isTransitionAnimationRunning by remember { mutableStateOf(false) } + + // Prevent quick screen changes to avoid some of the transition animation distortions + LaunchedEffect(currentScreen) { + snapshotFlow { isTransitionAnimationRunning } + .withIndex() + .map { (index, running) -> + if (running && index != 0) { + delay(timeMillis = 200) + } + running + } + .first { !it } + + currentStateProxy = currentScreen + } + // Restrict pressing the back button while screen transition is running to avoid most of the animation distortions + BackHandler(enabled = isTransitionAnimationRunning) {} + + // Box is needed to fix animation with resizing of AnimatedContent + Box(modifier = modifier.fillMaxSize()) { + AnimatedContent( + targetState = currentStateProxy, + contentAlignment = Alignment.TopCenter, + label = "Staking Screen Navigation", + transitionSpec = { + val direction = if (initialState.ordinal < targetState.ordinal) { + AnimatedContentTransitionScope.SlideDirection.Start + } else { + AnimatedContentTransitionScope.SlideDirection.End + } + + slideIntoContainer(towards = direction, animationSpec = tween()) + .togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween())) + }, + ) { state -> + isTransitionAnimationRunning = transition.targetState != transition.currentState + + when (state) { + StakingUiStateType.InitialInfo -> StakingInitialInfoContent( + state = uiState.initialInfoState, + ) + else -> Unit + } + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 1aec6214cd..30f890ae97 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.viewmodel internal interface StakingClickIntents { - // TODO staking + + fun onBackClick() } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 91cb3a2423..2c94bdea46 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -15,6 +16,8 @@ import com.tangem.features.staking.impl.presentation.state.StakingStateControlle import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.StakingStateRouter import com.tangem.features.staking.impl.presentation.state.transformers.HideBalanceStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.SetInitialDataStateTransformer +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* @@ -37,12 +40,15 @@ internal class StakingViewModel @Inject constructor( var stakingStateRouter: StakingStateRouter by Delegates.notNull() private set - private val cryptoCurrency: CryptoCurrency = savedStateHandle[StakingRouter.CRYPTO_CURRENCY_KEY] - ?: error("This screen can't open without `CryptoCurrency`") + private val cryptoCurrencyId: CryptoCurrency.ID = savedStateHandle[StakingRouter.CRYPTO_CURRENCY_ID_KEY] + ?: error("This screen can't be opened without `CryptoCurrency.ID`") private val userWalletId: UserWalletId = savedStateHandle.get(StakingRouter.USER_WALLET_ID_KEY) ?.let { stringValue -> UserWalletId(stringValue) } - ?: error("This screen can't open without `UserWalletId`") + ?: error("This screen can't be opened without `UserWalletId`") + + private val yield: Yield = savedStateHandle[StakingRouter.YIELD_KEY] + ?: error("This screen can't be opened without `Yield`") private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() @@ -53,16 +59,27 @@ internal class StakingViewModel @Inject constructor( subscribeOnCurrencyStatusUpdates() } - fun setRouter(router: InnerStakingRouter, stakingStateRouter: StakingStateRouter) { + override fun onBackClick() { + stakingStateRouter.onBackClick() + } + + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router - this.stakingStateRouter = stakingStateRouter + this.stakingStateRouter = stateRouter } private fun subscribeOnCurrencyStatusUpdates() { viewModelScope.launch { - getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrency.id).fold( + getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold( ifRight = { cryptoCurrencyStatus = it + stateController.update( + transformer = SetInitialDataStateTransformer( + clickIntents = this@StakingViewModel, + yield = yield, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + ), + ) }, ifLeft = { // TODO staking error diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index dc7674a9a7..514d108158 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -84,4 +84,6 @@ dependencies { /** Feature Apis */ implementation(projects.features.tokendetails.api) implementation(projects.features.send.api) + implementation(projects.features.staking.api) + } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 4b3900f2c3..f99dbb1cd3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -283,6 +283,7 @@ internal object TokenDetailsPreviewData { periodInDays = 4, tokenSymbol = "XLM", iconState = iconState, + onStakeClicked = {}, ), notifications = persistentListOf(), txHistoryState = TxHistoryState.NotSupported( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt index f1668235b6..76d9483c9b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt @@ -16,5 +16,6 @@ internal sealed interface StakingBlockState { val interestRate: String, val periodInDays: Int, val tokenSymbol: String, + val onStakeClicked: () -> Unit, ) : StakingBlockState } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index cec107a630..9f8de13d25 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -103,6 +103,7 @@ internal class TokenDetailsStateFactory( private val stakingStateConverter by lazy { TokenStakingStateConverter( currentStateProvider = currentStateProvider, + clickIntents = clickIntents, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt index 371e2e766d..0bdf52927f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt @@ -5,10 +5,12 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.Provider import com.tangem.utils.converter.Converter internal class TokenStakingStateConverter( + private val clickIntents: TokenDetailsClickIntents, private val currentStateProvider: Provider, ) : Converter, StakingBlockState> { @@ -28,6 +30,7 @@ internal class TokenStakingStateConverter( periodInDays = it.periodInDays, tokenSymbol = it.tokenSymbol, iconState = currentStateProvider().tokenInfoBlockState.iconState, + onStakeClicked = clickIntents::onStakeBannerClick, ) }, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt index 6ce576304f..d888903a28 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt @@ -121,8 +121,8 @@ private fun StakingContent(stakingBlockState: StakingBlockState.Content, iconSta } SecondaryButton( modifier = Modifier.fillMaxWidth(), - text = "Stake", - onClick = { /* [REDACTED_TODO_COMMENT] */ }, + text = stringResource(id = R.string.common_stake), + onClick = stakingBlockState.onStakeClicked, ) } } @@ -191,6 +191,7 @@ private class StakingBlockStateProvider : CollectionPreviewParameterProvider