Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-18 21:26:16 +03:00
parent 60258b3096
commit fa948bd91a
28 changed files with 651 additions and 83 deletions

View file

@ -30,4 +30,7 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.appCurrency.models)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}
}

View file

@ -0,0 +1,101 @@
package com.tangem.common.ui.amountScreen.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
@Composable
fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDisabled: Boolean, onClick: () -> Unit) {
if (amountState !is AmountState.Data) return
val amount = amountState.amountTextField
val cryptoAmount = BigDecimalFormatter.formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol)
val fiatAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = amount.fiatAmount.value,
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
fiatCurrencyCode = amountState.appCurrencyCode,
)
val backgroundColor = if (isEditingDisabled) {
TangemTheme.colors.button.disabled
} else {
TangemTheme.colors.background.action
}
val (firstAmount, secondAmount) = if (amount.isFiatValue) {
fiatAmount to cryptoAmount
} else {
cryptoAmount to fiatAmount
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(backgroundColor)
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
.padding(TangemTheme.dimens.spacing16),
) {
TokenIcon(state = amountState.tokenIconState)
ResizableText(
text = firstAmount,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing24),
)
Text(
text = secondAmount,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing8),
)
}
}
// region Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::class) value: AmountState) {
TangemThemePreview {
AmountBlock(
amountState = value,
isClickDisabled = false,
isEditingDisabled = false,
onClick = {},
)
}
}
private class AmountBlockPreviewProvider : PreviewParameterProvider<AmountState> {
override val values: Sequence<AmountState>
get() = sequenceOf(
AmountStatePreviewData.amountState,
)
}
// endregion

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.utils
package com.tangem.common.ui.amountScreen.utils
import com.tangem.blockchain.common.Amount
import com.tangem.core.ui.extensions.TextReference
@ -11,7 +11,7 @@ import java.math.BigDecimal
private const val CRYPTO_FEE_DECIMALS = 6
internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? {
fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? {
if (amount == null) return null
return combinedReference(
if (isFeeApproximate) stringReference("${BigDecimalFormatter.CAN_BE_LOWER_SIGN} ") else TextReference.EMPTY,
@ -25,13 +25,13 @@ internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): Tex
)
}
internal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? {
fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? {
if (value == null || rate == null) return null
val formattedFiat = getFiatString(value = value, rate = rate, appCurrency = appCurrency)
return stringReference(formattedFiat)
}
internal fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String {
fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String {
if (value == null || rate == null) return EMPTY_BALANCE_SIGN
val feeValue = value.multiply(rate)
return BigDecimalFormatter.formatFiatAmount(

View file

@ -543,6 +543,7 @@
<string name="send_transaction_success">Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время</string>
<string name="send_validation_invalid_address">Неверный адрес</string>
<string name="sent_transaction_sent_title">Транзакция отправлена</string>
<string name="settings_card_settings_footer">Подготовьтесь к сканированию карты, которую вы хотите настроить.</string>
<string name="settings_forget_wallet">Забыть кошелек</string>
<string name="settings_forget_wallet_footer">Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова.</string>
<string name="settings_wallet_name_title">Имя</string>

View file

@ -115,7 +115,7 @@
<string name="common_origin_card">Primary Card</string>
<string name="common_passphrase">Passphrase</string>
<string name="common_paste">Paste</string>
<string name="common_percent_range" formatted="false">%1$s-%2$s</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_read_more">Read more</string>
<string name="common_receive">Receive</string>
<string name="common_reject">Reject</string>
@ -310,7 +310,12 @@
<string name="manage_tokens_unavailable_vote">Upvote</string>
<string name="manage_tokens_wallet_selector_title">Choose wallet</string>
<string name="manage_tokens_wallet_support_only_one_network_title">The wallet doesn\'t support more than one network</string>
<string name="markets_add_to_my_portfolio_description">To start buying, exchanging or receiving this asset, add this token to at least 1 network</string>
<string name="markets_add_to_my_portfolio_unavailable_description">This asset is not available</string>
<string name="markets_add_to_portfolio_button">Add to portfolio</string>
<string name="markets_common_my_portfolio">My portfolio</string>
<string name="markets_common_title">Market</string>
<string name="markets_select_wallet">Select wallet</string>
<string name="markets_sort_by_title">Sort By</string>
<string name="onboarding_access_code_feature_1_description">You have to set up a single access code to protect all your cards</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
@ -541,6 +546,7 @@
<string name="send_validation_invalid_address">Invalid address</string>
<string name="send_wallet_balance_format">%1$s (%2$s)</string>
<string name="sent_transaction_sent_title">Transaction sent</string>
<string name="settings_card_settings_footer">Prepare to scan card you want to setup.</string>
<string name="settings_forget_wallet">Forget wallet</string>
<string name="settings_forget_wallet_footer">This will remove the wallet from the application. The wallet itself can be added again.</string>
<string name="settings_wallet_name_title">Name</string>
@ -559,7 +565,10 @@
<string name="staking_details_title">Staking %s</string>
<string name="staking_details_unbonding_period">Unbonding Period</string>
<string name="staking_details_warmup_period">Warmup period</string>
<string name="staking_notification_earn_rewards_text" formatted="false">Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days.</string>
<string name="staking_notification_earn_rewards_title">Earn staking rewards</string>
<string name="staking_rewards">Rewards</string>
<string name="staking_validator">Validator</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
<string name="story_awe_title">Revolutionary Hardware Wallet</string>
<string name="story_backup_description">Up to 3 physical cards to one wallet</string>

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.minimalAmount
import com.tangem.common.ui.amountScreen.utils.getFiatString
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.utils.BigDecimalFormatter
@ -25,7 +26,6 @@ import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import com.tangem.features.send.impl.presentation.state.*
import com.tangem.features.send.impl.presentation.state.fee.*
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.utils.getFiatString
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.lib.crypto.BlockchainUtils.isTezos

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
@ -14,7 +15,6 @@ import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.utils.getFiatReference
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.Provider

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
@ -14,7 +15,6 @@ import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.utils.getFiatReference
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList

View file

@ -21,6 +21,8 @@ import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import com.tangem.common.ui.amountScreen.utils.getCryptoReference
import com.tangem.common.ui.amountScreen.utils.getFiatString
import com.tangem.core.ui.R
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SecondaryButtonIconStart
@ -32,12 +34,9 @@ import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.tokens.model.Amount
import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.SendUiStateType
import com.tangem.features.send.impl.presentation.utils.getFiatString
import com.tangem.features.send.impl.presentation.utils.getCryptoReference
@Composable
internal fun SendNavigationButtons(

View file

@ -11,6 +11,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.utils.getCryptoReference
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.rows.SelectorRowItem
@ -20,8 +22,6 @@ import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.utils.getCryptoReference
import com.tangem.features.send.impl.presentation.utils.getFiatReference
@Composable
internal fun SendSpeedSelectorItem(

View file

@ -14,6 +14,8 @@ 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 com.tangem.common.ui.amountScreen.utils.getCryptoReference
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.res.TangemThemePreview
@ -24,8 +26,6 @@ import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.state.previewdata.FeeStatePreviewData
import com.tangem.features.send.impl.presentation.utils.getCryptoReference
import com.tangem.features.send.impl.presentation.utils.getFiatReference
@Composable
internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, onClick: () -> Unit) {

View file

@ -60,6 +60,7 @@ dependencies {
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.transaction)
/** Common */
implementation(projects.common.ui)

View file

@ -0,0 +1,16 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.blockchain.common.transaction.TransactionFee
@Immutable
internal sealed class InnerFeeState {
data class Content(
val fees: TransactionFee,
) : InnerFeeState()
data object Loading : InnerFeeState()
data object Error : InnerFeeState()
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.domain.staking.model.Yield
@Immutable
internal sealed class InnerValidatorState {
data class Content(
val chosenValidator: Yield.Validator,
) : InnerValidatorState()
data object Loading : InnerValidatorState()
data object Error : InnerValidatorState()
}

View file

@ -0,0 +1,54 @@
package com.tangem.features.staking.impl.presentation.state
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.staking.impl.R
internal sealed class StakingNotification(val config: NotificationConfig) {
sealed class Error(
title: TextReference,
subtitle: TextReference,
iconResId: Int = R.drawable.ic_alert_24,
buttonState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : StakingNotification(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = iconResId,
buttonsState = buttonState,
onCloseClick = onCloseClick,
),
) {
// TODO staking
}
sealed class Warning(
title: TextReference,
subtitle: TextReference,
buttonsState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : StakingNotification(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = R.drawable.ic_alert_circle_24,
buttonsState = buttonsState,
onCloseClick = onCloseClick,
),
) {
data class EarnRewards(
val currencyName: String,
val days: Int,
) : Warning(
title = resourceReference(R.string.staking_notification_earn_rewards_title),
subtitle = resourceReference(
R.string.staking_notification_earn_rewards_text,
wrappedList(currencyName, days),
),
)
}
}

View file

@ -39,7 +39,6 @@ internal class StakingStateController @Inject constructor() {
currentStep = StakingStep.InitialInfo,
initialInfoState = StakingStates.InitialInfoState.Empty(),
amountState = AmountState.Empty(),
validatorState = StakingStates.ValidatorState.Empty(),
confirmStakingState = StakingStates.ConfirmStakingState.Empty(),
isBalanceHidden = false,
event = consumedEvent(),

View file

@ -1,7 +1,6 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.fragment.app.FragmentManager
import kotlinx.coroutines.flow.update
import java.lang.ref.WeakReference
internal class StakingStateRouter(
@ -22,7 +21,7 @@ internal class StakingStateRouter(
isSuccess -> popBackStack()
else -> when (type) {
StakingStep.Amount -> showInitial()
StakingStep.ValidatorAndFee -> showAmount()
StakingStep.Confirm -> showAmount()
else -> popBackStack()
}
}
@ -31,9 +30,9 @@ internal class StakingStateRouter(
fun onNextClick() {
when (stateController.uiState.value.currentStep) {
StakingStep.InitialInfo -> showAmount()
StakingStep.Amount -> showValidator()
StakingStep.ValidatorAndFee -> showConfirm()
StakingStep.Confirm -> onBackClick()
StakingStep.Amount -> showConfirm()
StakingStep.Confirm -> showSuccess()
StakingStep.Success -> onBackClick()
}
}
@ -52,13 +51,13 @@ internal class StakingStateRouter(
stateController.update { it.copy(currentStep = StakingStep.Amount) }
}
fun showValidator() {
stateController.update { it.copy(currentStep = StakingStep.ValidatorAndFee) }
}
fun showConfirm() {
stateController.update { it.copy(currentStep = StakingStep.Confirm) }
}
fun showSuccess() {
stateController.update { it.copy(currentStep = StakingStep.Success) }
}
private fun getInitialState() = StakingStep.InitialInfo
}

View file

@ -1,10 +1,15 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.Yield
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
/**
* Ui states of the staking screen
@ -16,7 +21,6 @@ internal data class StakingUiState(
val currentStep: StakingStep,
val initialInfoState: StakingStates.InitialInfoState,
val amountState: AmountState,
val validatorState: StakingStates.ValidatorState,
val confirmStakingState: StakingStates.ConfirmStakingState,
val isBalanceHidden: Boolean,
val event: StateEvent<StakingEvent>,
@ -25,12 +29,10 @@ internal data class StakingUiState(
fun copyWrapped(
initialInfoState: StakingStates.InitialInfoState = this.initialInfoState,
amountState: AmountState = this.amountState,
validatorState: StakingStates.ValidatorState = this.validatorState,
confirmStakingState: StakingStates.ConfirmStakingState = this.confirmStakingState,
): StakingUiState = copy(
initialInfoState = initialInfoState,
amountState = amountState,
validatorState = validatorState,
confirmStakingState = confirmStakingState,
)
}
@ -58,32 +60,14 @@ internal sealed class StakingStates {
) : InitialInfoState()
}
/** Validator state */
sealed class ValidatorState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
) : ValidatorState()
data class Empty(
override val isPrimaryButtonEnabled: Boolean = false,
) : ValidatorState()
}
/** Fee state */
sealed class FeeState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
) : FeeState()
data class Empty(
override val isPrimaryButtonEnabled: Boolean = false,
) : FeeState()
}
/** Confirm state */
sealed class ConfirmStakingState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
val feeState: FeeState,
val validatorState: ValidatorState,
val notifications: ImmutableList<StakingNotification>,
val footerText: String,
val isSuccess: Boolean,
val isStaking: Boolean,
) : ConfirmStakingState()
@ -92,11 +76,25 @@ internal sealed class StakingStates {
override val isPrimaryButtonEnabled: Boolean = false,
) : ConfirmStakingState()
}
data class FeeState(
val innerFeeState: InnerFeeState,
val fee: Fee?,
val rate: BigDecimal?,
val isFeeConvertibleToFiat: Boolean,
val appCurrency: AppCurrency,
val isFeeApproximate: Boolean,
)
data class ValidatorState(
val validatorState: InnerValidatorState,
val availableValidators: List<Yield.Validator>,
)
}
enum class StakingStep {
InitialInfo,
Amount,
ValidatorAndFee,
Confirm,
Success,
}

View file

@ -0,0 +1,92 @@
package com.tangem.features.staking.impl.presentation.state.previewdata
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType.Coin
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.Yield
import com.tangem.features.staking.impl.presentation.state.InnerFeeState
import com.tangem.features.staking.impl.presentation.state.InnerValidatorState
import com.tangem.features.staking.impl.presentation.state.StakingNotification
import com.tangem.features.staking.impl.presentation.state.StakingStates
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
internal object ConfirmStakingStatePreviewData {
private val validatorList = listOf(
Yield.Validator(
address = "0xa6e768fef2d1af36c0cfdb276422e7881a83e951",
status = "active",
name = "Luganodes",
image = "https://assets.stakek.it/validators/luganodes.png",
apr = BigDecimal("0.054823398040640445"),
commission = 0.1,
stakedBalance = "355544384.45009977",
website = "https://luganodes.com/",
votingPower = 0.09778360195377911,
preferred = true,
),
Yield.Validator(
address = "0x35b1ca0f398905cf752e6fe122b51c88022fca32",
status = "active",
name = "InfStones",
image = "https://assets.stakek.it/validators/infstones.png",
apr = BigDecimal("0.057786472172836965"),
commission = 0.05,
stakedBalance = "12495684.05643019",
website = "https://infstones.com/",
votingPower = 0.0034366257754399774,
preferred = true,
),
Yield.Validator(
address = "0xd14a87025109013b0a2354a775cb335f926af65a",
status = "active",
name = "Kiln",
image = "https://assets.stakek.it/validators/kiln.png",
apr = BigDecimal("0.057786472172836965"),
commission = 0.05,
stakedBalance = "85400369.96393165",
website = "https://infstones.com/",
votingPower = 0.023487238579718264,
preferred = true,
),
)
private val fee = Fee.Common(
amount = Amount(
currencySymbol = "MATIC",
value = BigDecimal(0.159806),
decimals = 18,
type = Coin,
),
)
val confirmStakingState = StakingStates.ConfirmStakingState.Data(
isPrimaryButtonEnabled = true,
feeState = StakingStates.FeeState(
innerFeeState = InnerFeeState.Content(TransactionFee.Single(fee)),
fee = fee,
rate = BigDecimal.ONE,
appCurrency = AppCurrency.Default,
isFeeApproximate = false,
isFeeConvertibleToFiat = true,
),
validatorState = StakingStates.ValidatorState(
validatorState = InnerValidatorState.Content(
chosenValidator = validatorList[0],
),
availableValidators = validatorList,
),
footerText = "You stake \$715.11 and will be receiving ~\$35 monthly",
notifications = persistentListOf(
StakingNotification.Warning.EarnRewards(
currencyName = "Solana",
days = 2,
),
),
isStaking = false,
isSuccess = false,
)
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData
import com.tangem.utils.Provider
import com.tangem.utils.transformer.Transformer
@Suppress("UnusedPrivateMember")
internal class SetConfirmStateDataStateTransformer(
private val yield: Yield,
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
// TODO staking fill with real data
return prevState.copy(
confirmStakingState = ConfirmStakingStatePreviewData.confirmStakingState,
)
}
private fun isFeeApproximate(fee: Fee): Boolean {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
return isFeeApproximateUseCase(
networkId = cryptoCurrencyStatus.currency.network.id,
amountType = fee.amount.type,
)
}
}

View file

@ -93,7 +93,7 @@ internal class SetInitialDataStateTransformer(
if (maxApr - minApr < EQUALITY_THRESHOLD) {
return stringReference("$formattedMinApr%")
}
return resourceReference(R.string.common_percent_range, wrappedList(formattedMinApr, formattedMaxApr))
return resourceReference(R.string.common_range, wrappedList(formattedMinApr, formattedMaxApr))
}
companion object {

View file

@ -0,0 +1,72 @@
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.common.ui.amountScreen.ui.AmountBlock
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData
import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock
import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock
@Composable
internal fun StakingConfirmContent(amountState: AmountState, state: StakingStates.ConfirmStakingState) {
if (state !is StakingStates.ConfirmStakingState.Data) return
Column(
modifier = Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing16)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
AmountBlock(
amountState = amountState,
isClickDisabled = true,
isEditingDisabled = true,
onClick = {},
)
StakingFeeBlock(feeState = state.feeState)
NotificationsBlock(notifications = state.notifications)
SpacerHMax()
FooterText(text = state.footerText)
}
}
@Composable
private fun FooterText(text: String) {
Text(
modifier = Modifier.fillMaxWidth(),
text = text,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
textAlign = TextAlign.Center,
)
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_StakingConfirmContent() {
TangemThemePreview {
Column(Modifier.background(TangemTheme.colors.background.primary)) {
StakingConfirmContent(
amountState = AmountStatePreviewData.amountState,
state = ConfirmStakingStatePreviewData.confirmStakingState,
)
}
}
}

View file

@ -51,8 +51,8 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier
val isSuccess = (confirmState as? StakingStates.ConfirmStakingState.Data)?.isSuccess ?: false
val isStaking = (confirmState as? StakingStates.ConfirmStakingState.Data)?.isStaking ?: false
val isButtonsVisible = uiState.currentStep != StakingStep.Confirm
val isStakingState = uiState.currentStep == StakingStep.Confirm && !isSuccess && !isStaking
val isButtonsVisible = uiState.currentStep != StakingStep.Success
val isStakingState = uiState.currentStep == StakingStep.Success && !isSuccess && !isStaking
val (buttonTextId, buttonClick) = getButtonData(
currentState = uiState,
@ -103,8 +103,8 @@ private fun getButtonData(currentState: StakingUiState): Pair<Int, () -> Unit> {
return when (currentState.currentStep) {
StakingStep.InitialInfo,
StakingStep.Amount,
StakingStep.ValidatorAndFee,
StakingStep.Confirm,
StakingStep.Success,
-> R.string.common_next to { currentState.clickIntents.onNextClick() }
}
}
@ -113,7 +113,7 @@ private fun isButtonEnabled(uiState: StakingUiState): Boolean {
return when (uiState.currentStep) {
StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled
StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled
StakingStep.ValidatorAndFee -> uiState.validatorState.isPrimaryButtonEnabled
StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled
StakingStep.Success -> true
}
}

View file

@ -52,13 +52,13 @@ private fun SendAppBar(uiState: StakingUiState) {
val titleRes = when (uiState.currentStep) {
StakingStep.InitialInfo -> stringResource(id = R.string.common_stake)
StakingStep.Amount -> stringResource(id = R.string.send_amount_label)
StakingStep.ValidatorAndFee -> stringResource(id = R.string.common_stake)
StakingStep.Confirm -> ""
StakingStep.Confirm -> stringResource(id = R.string.common_stake)
StakingStep.Success -> ""
}
val backIcon = when (uiState.currentStep) {
StakingStep.Amount,
StakingStep.ValidatorAndFee,
StakingStep.Confirm,
StakingStep.Success,
-> {
R.drawable.ic_close_24
}
@ -127,8 +127,9 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M
isBalanceHiding = uiState.isBalanceHidden,
clickIntents = uiState.clickIntents,
)
StakingStep.ValidatorAndFee -> StakingValidatorAndFeeContent(
state = uiState.validatorState,
StakingStep.Confirm -> StakingConfirmContent(
amountState = uiState.amountState,
state = uiState.confirmStakingState,
)
else -> TODO()
}

View file

@ -1,11 +0,0 @@
package com.tangem.features.staking.impl.presentation.ui
import androidx.compose.runtime.Composable
import com.tangem.features.staking.impl.presentation.state.StakingStates
@Composable
internal fun StakingValidatorAndFeeContent(state: StakingStates.ValidatorState) {
if (state !is StakingStates.ValidatorState.Data) return
// TODO staking
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.staking.impl.presentation.ui.block
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.staking.impl.presentation.state.StakingNotification
@Composable
internal fun NotificationsBlock(notifications: List<StakingNotification>) {
notifications.forEach {
Notification(config = it.config, iconTint = TangemTheme.colors.icon.accent)
}
}

View file

@ -0,0 +1,148 @@
package com.tangem.features.staking.impl.presentation.ui.block
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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 com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.utils.getCryptoReference
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.staking.impl.presentation.state.InnerFeeState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import java.math.BigDecimal
@Composable
internal fun StakingFeeBlock(feeState: StakingStates.FeeState) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = stringResource(R.string.common_network_fee_title),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
)
Box(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
) {
val feeAmount = feeState.fee?.amount
val (title, icon) = R.string.common_fee_selector_option_market to R.drawable.ic_bird_24
SelectorRowItem(
titleRes = title,
iconRes = icon,
preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate),
postDot = if (feeState.isFeeConvertibleToFiat) {
getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency)
} else {
null
},
ellipsizeOffset = feeAmount?.currencySymbol?.length,
isSelected = true,
showDivider = false,
showSelectedAppearance = false,
paddingValues = PaddingValues(),
)
FeeLoading(feeState.innerFeeState)
FeeError(feeState.innerFeeState)
}
}
}
@Composable
private fun BoxScope.FeeLoading(feeSelectorState: InnerFeeState) {
AnimatedContent(
targetState = feeSelectorState,
label = "Fee Loading State Change",
modifier = Modifier.align(Alignment.CenterEnd),
) {
if (it == InnerFeeState.Loading) {
RectangleShimmer(
radius = TangemTheme.dimens.radius3,
modifier = Modifier.size(
height = TangemTheme.dimens.size12,
width = TangemTheme.dimens.size90,
),
)
}
}
}
@Composable
private fun BoxScope.FeeError(feeSelectorState: InnerFeeState) {
AnimatedContent(
targetState = feeSelectorState,
label = "Fee Error State Change",
modifier = Modifier.align(Alignment.CenterEnd),
) {
if (it == InnerFeeState.Error) {
Text(
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
)
}
}
}
// region Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: StakingStates.FeeState) {
TangemThemePreview {
StakingFeeBlock(
feeState = value,
)
}
}
private class FeeBlockPreviewProvider : PreviewParameterProvider<StakingStates.FeeState> {
override val values: Sequence<StakingStates.FeeState>
get() = sequenceOf(
feeState,
)
private val fee = Fee.Common(
amount = Amount(
currencySymbol = "MATIC",
value = BigDecimal(0.159806),
decimals = 18,
type = AmountType.Coin,
),
)
private val feeState = StakingStates.FeeState(
innerFeeState = InnerFeeState.Content(TransactionFee.Single(normal = fee)),
fee = fee,
rate = BigDecimal.ONE,
appCurrency = AppCurrency.Default,
isFeeApproximate = false,
isFeeConvertibleToFiat = true,
)
}
// endregion

View file

@ -15,14 +15,17 @@ 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
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.staking.impl.navigation.InnerStakingRouter
import com.tangem.features.staking.impl.presentation.state.StakingStateController
import com.tangem.features.staking.impl.presentation.state.StakingStateRouter
import com.tangem.features.staking.impl.presentation.state.StakingStep
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.transformers.HideBalanceStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.SetConfirmStateDataStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.SetInitialDataStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer
@ -45,6 +48,7 @@ internal class StakingViewModel @Inject constructor(
private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents {
@ -72,9 +76,10 @@ internal class StakingViewModel @Inject constructor(
private var innerRouter: InnerStakingRouter by Delegates.notNull()
private var userWallet: UserWallet by Delegates.notNull()
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private var appCurrency: AppCurrency by Delegates.notNull()
init {
subscribeOnSelectedAppCurrency()
subscribeOnBalanceHiding()
subscribeOnCurrencyStatusUpdates()
}
@ -85,6 +90,27 @@ internal class StakingViewModel @Inject constructor(
override fun onNextClick() {
stakingStateRouter.onNextClick()
when (value.currentStep) {
StakingStep.Confirm -> {
stateController.update(
SetConfirmStateDataStateTransformer(
yield = yield,
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
isFeeApproximateUseCase = isFeeApproximateUseCase,
),
)
}
StakingStep.InitialInfo -> {
// TODO staking
}
StakingStep.Amount -> {
// TODO staking
}
StakingStep.Success -> {
// TODO staking
}
}
}
override fun onPrevClick() {
@ -131,7 +157,7 @@ internal class StakingViewModel @Inject constructor(
yield = yield,
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
userWalletProvider = Provider { userWallet },
appCurrencyProvider = Provider { selectedAppCurrencyFlow.value },
appCurrencyProvider = Provider { appCurrency },
),
)
},
@ -153,18 +179,14 @@ internal class StakingViewModel @Inject constructor(
.launchIn(viewModelScope)
}
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
private fun subscribeOnSelectedAppCurrency() {
getSelectedAppCurrencyUseCase()
.conflate()
.distinctUntilChanged()
.map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
.onEach { maybeAppCurrency ->
appCurrency = maybeAppCurrency.getOrElse { AppCurrency.Default }
}
.flowOn(dispatchers.main)
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
.launchIn(viewModelScope)
}
}