Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-12 15:46:47 +03:00
parent 59ad4c5c5c
commit 98ec93e259
34 changed files with 407 additions and 210 deletions

View file

@ -0,0 +1,73 @@
package com.tangem.common.ui.amountScreen.ui
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.extensions.shareText
import com.tangem.core.ui.res.TangemTheme
import com.tangem.common.ui.R
import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun SendDoneButtons(
txUrl: String,
onExploreClick: () -> Unit,
onShareClick: () -> Unit,
isVisible: Boolean,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
val context = LocalContext.current
AnimatedVisibility(
visible = isVisible && txUrl.isNotBlank(),
modifier = modifier,
enter = slideInVertically().plus(fadeIn()),
exit = slideOutVertically().plus(fadeOut()),
label = "Animate show sent state buttons",
) {
Row(modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12)) {
SecondaryButtonIconStart(
text = stringResource(id = R.string.common_explore),
iconResId = R.drawable.ic_web_24,
onClick = onExploreClick,
modifier = Modifier.weight(1f),
)
SpacerW12()
SecondaryButtonIconStart(
text = stringResource(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(txUrl)
onShareClick()
},
modifier = Modifier.weight(1f),
)
}
}
}
@Preview(showBackground = true, widthDp = 328)
@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SendDoneButtons_Preview() {
TangemThemePreview {
SendDoneButtons(
txUrl = "txUrl",
onShareClick = {},
onExploreClick = {},
isVisible = true,
)
}
}

View file

@ -30,8 +30,7 @@ internal fun InputRowImageBase(
InputRowAsyncImage(
imageUrl = imageUrl,
modifier = Modifier
.size(TangemTheme.dimens.spacing36)
.padding(vertical = TangemTheme.dimens.size1),
.size(TangemTheme.dimens.spacing36),
)
Column {
Text(

View file

@ -35,6 +35,7 @@ fun InputRowImageChevron(
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
showChevron: Boolean = true,
) {
InputRowImageBase(
subtitle = subtitle,
@ -45,11 +46,13 @@ fun InputRowImageChevron(
captionColor = captionColor,
) {
SpacerWMax()
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
if (showChevron) {
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}
}

View file

@ -17,6 +17,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.atoms.radiobutton.TangemRadioButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.annotatedReference
@ -65,13 +66,14 @@ fun InputRowImageSelector(
)
.padding(TangemTheme.dimens.spacing12),
) {
SpacerWMax()
TangemRadioButton(isSelected = isSelected, isEnabled = false, onClick = onSelect)
}
}
//region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(widthDp = 328)
@Preview(widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun InputRowImageSelectorPreview(
@PreviewParameter(InputRowImageSelectorPreviewDataProvider::class) data: InputRowImageSelectorPreviewData,

View file

@ -122,7 +122,7 @@ data class TangemDimens internal constructor(
val spacing30: Dp = 30.dp,
val spacing32: Dp = 32.dp,
val spacing34: Dp = 34.dp,
val spacing36: Dp = 34.dp,
val spacing36: Dp = 36.dp,
val spacing38: Dp = 38.dp,
val spacing40: Dp = 40.dp,
val spacing44: Dp = 44.dp,

View file

@ -11,7 +11,6 @@ import com.tangem.domain.wallets.models.UserWalletId
class GetExplorerTransactionUrlUseCase(
private val repository: TxHistoryRepository,
) {
@Deprecated("Replace with invoke [UserWalletId, Network]")
operator fun invoke(txHash: String, networkId: Network.ID): Either<TxStatusError, String> {
return either {
catch(

View file

@ -16,17 +16,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
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.models.AmountState
import com.tangem.common.ui.amountScreen.ui.SendDoneButtons
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
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
@ -220,46 +219,6 @@ private fun SendingText(
}
}
@Composable
private fun SendDoneButtons(
txUrl: String,
onExploreClick: () -> Unit,
onShareClick: () -> Unit,
isVisible: Boolean,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
val context = LocalContext.current
AnimatedVisibility(
visible = isVisible && txUrl.isNotBlank(),
modifier = modifier,
enter = slideInVertically().plus(fadeIn()),
exit = slideOutVertically().plus(fadeOut()),
label = "Animate show sent state buttons",
) {
Row(modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12)) {
SecondaryButtonIconStart(
text = stringResource(id = R.string.common_explore),
iconResId = R.drawable.ic_web_24,
onClick = onExploreClick,
modifier = Modifier.weight(1f),
)
SpacerW12()
SecondaryButtonIconStart(
text = stringResource(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(txUrl)
onShareClick()
},
modifier = Modifier.weight(1f),
)
}
}
}
private fun getButtonData(
uiState: SendUiState,
currentState: SendUiCurrentScreen,

View file

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

View file

@ -1,5 +1,6 @@
package com.tangem.features.staking.impl.di
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.features.staking.impl.navigation.DefaultStakingRouter
import com.tangem.features.staking.api.navigation.StakingRouter
import dagger.Module
@ -17,7 +18,9 @@ internal object StakingRouterModule {
@Provides
@ActivityScoped
fun provideStakingRouter(): StakingRouter {
return DefaultStakingRouter()
fun provideStakingRouter(urlOpener: UrlOpener): StakingRouter {
return DefaultStakingRouter(
urlOpener = urlOpener,
)
}
}

View file

@ -1,9 +1,15 @@
package com.tangem.features.staking.impl.navigation
import androidx.fragment.app.Fragment
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.features.staking.impl.presentation.StakingFragment
internal class DefaultStakingRouter : InnerStakingRouter {
internal class DefaultStakingRouter(
private val urlOpener: UrlOpener,
) : InnerStakingRouter {
override fun getEntryFragment(): Fragment = StakingFragment.create()
override fun openUrl(url: String) {
urlOpener.openUrl(url)
}
}

View file

@ -4,5 +4,5 @@ import com.tangem.features.staking.api.navigation.StakingRouter
interface InnerStakingRouter : StakingRouter {
// TODO staking
fun openUrl(url: String)
}

View file

@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
@ -15,7 +16,6 @@ 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
import javax.inject.Inject
/**
@ -28,7 +28,10 @@ internal class StakingFragment : ComposeFragment() {
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var router: StakingRouter
lateinit var stakingRouter: StakingRouter
@Inject
lateinit var appRouter: AppRouter
@Inject
lateinit var stateController: StakingStateController
@ -38,7 +41,7 @@ internal class StakingFragment : ComposeFragment() {
private val viewModel by viewModels<StakingViewModel>()
private val innerStakingRouter: InnerStakingRouter
get() = requireNotNull(router as? InnerStakingRouter) {
get() = requireNotNull(stakingRouter as? InnerStakingRouter) {
"innerStakingRouter should be instance of InnerStakingRouter"
}
@ -49,7 +52,7 @@ internal class StakingFragment : ComposeFragment() {
viewModel.setRouter(
innerStakingRouter,
StakingStateRouter(
fragmentManager = WeakReference(parentFragmentManager),
appRouter = appRouter,
stateController = stateController,
),
)

View file

@ -0,0 +1,7 @@
package com.tangem.features.staking.impl.presentation.state
enum class InnerConfirmationStakingState {
ASSENT,
IN_PROGRESS,
COMPLETED,
}

View file

@ -40,7 +40,7 @@ internal class StakingStateController @Inject constructor() {
initialInfoState = StakingStates.InitialInfoState.Empty(),
amountState = AmountState.Empty(),
rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(),
confirmStakingState = StakingStates.ConfirmStakingState.Empty(),
confirmationState = StakingStates.ConfirmationState.Empty(),
isBalanceHidden = false,
event = consumedEvent(),
bottomSheetConfig = null,

View file

@ -1,49 +1,37 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.fragment.app.FragmentManager
import java.lang.ref.WeakReference
import com.tangem.common.routing.AppRouter
internal class StakingStateRouter(
private val fragmentManager: WeakReference<FragmentManager>,
private val appRouter: AppRouter,
private val stateController: StakingStateController,
) {
private fun closeStaking() {
fragmentManager.get()?.popBackStack()
fun onBackClick() {
appRouter.pop()
stateController.clear()
}
fun onBackClick(isSuccess: Boolean = false) {
val type = stateController.uiState.value.currentStep
when {
isSuccess -> closeStaking()
else -> when (type) {
StakingStep.Amount -> showInitial()
StakingStep.Confirm -> showAmount()
else -> closeStaking()
}
}
}
fun onNextClick() {
when (stateController.uiState.value.currentStep) {
StakingStep.InitialInfo -> showAmount()
StakingStep.RewardsValidators,
StakingStep.Validators,
StakingStep.Amount,
-> showConfirm()
StakingStep.Confirm -> showSuccess()
StakingStep.Success -> closeStaking()
-> showConfirmation()
StakingStep.Confirmation -> {
// TODO staking handle
}
}
}
fun onPrevClick() {
when (stateController.uiState.value.currentStep) {
StakingStep.InitialInfo -> onBackClick()
StakingStep.Amount -> showInitial()
StakingStep.Confirm -> showAmount()
StakingStep.Success -> closeStaking()
StakingStep.Validators -> showConfirm()
else -> closeStaking()
StakingStep.Confirmation -> showAmount()
StakingStep.Validators -> showConfirmation()
StakingStep.RewardsValidators -> showInitial()
}
}
@ -63,12 +51,7 @@ internal class StakingStateRouter(
stateController.update { it.copy(currentStep = StakingStep.Validators) }
}
fun showConfirm() {
stateController.update { it.copy(currentStep = StakingStep.Confirm) }
}
fun showSuccess() {
// stateController.update { it.copy(currentStep = StakingStep.Success) }
// TODO staking [REDACTED_TASK_KEY]
fun showConfirmation() {
stateController.update { it.copy(currentStep = StakingStep.Confirmation) }
}
}

View file

@ -20,7 +20,7 @@ internal data class StakingUiState(
val initialInfoState: StakingStates.InitialInfoState,
val amountState: AmountState,
val rewardsValidatorsState: StakingStates.RewardsValidatorsState,
val confirmStakingState: StakingStates.ConfirmStakingState,
val confirmationState: StakingStates.ConfirmationState,
val isBalanceHidden: Boolean,
val bottomSheetConfig: TangemBottomSheetConfig?,
val event: StateEvent<StakingEvent>,
@ -29,11 +29,11 @@ internal data class StakingUiState(
fun copyWrapped(
initialInfoState: StakingStates.InitialInfoState = this.initialInfoState,
amountState: AmountState = this.amountState,
confirmStakingState: StakingStates.ConfirmStakingState = this.confirmStakingState,
confirmationState: StakingStates.ConfirmationState = this.confirmationState,
): StakingUiState = copy(
initialInfoState = initialInfoState,
amountState = amountState,
confirmStakingState = confirmStakingState,
confirmationState = confirmationState,
)
}
@ -74,27 +74,21 @@ internal sealed class StakingStates {
) : RewardsValidatorsState()
}
/** Confirm state */
sealed class ConfirmStakingState : StakingStates() {
/** Confirmation state */
sealed class ConfirmationState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
val innerState: InnerConfirmationStakingState,
val feeState: FeeState,
val validatorState: ValidatorState,
val notifications: ImmutableList<StakingNotification>,
val footerText: String,
val innerState: InnerConfirmStakingState,
) : ConfirmStakingState() {
enum class InnerConfirmStakingState {
CONFIRM,
IN_PROGRESS,
SUCCESS,
}
}
val transactionDoneState: TransactionDoneState,
) : ConfirmationState()
data class Empty(
override val isPrimaryButtonEnabled: Boolean = false,
) : ConfirmStakingState()
) : ConfirmationState()
}
}
@ -102,7 +96,6 @@ enum class StakingStep {
InitialInfo,
RewardsValidators,
Amount,
Confirm,
Confirmation,
Validators,
Success,
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
@Immutable
internal sealed class TransactionDoneState {
data class Content(
val timestamp: Long,
val txUrl: String,
) : TransactionDoneState()
data object Empty : TransactionDoneState()
}

View file

@ -6,12 +6,29 @@ import com.tangem.domain.staking.model.Yield
@Immutable
internal sealed class ValidatorState {
abstract val isClickable: Boolean
data class Content(
override val isClickable: Boolean,
val chosenValidator: Yield.Validator,
val availableValidators: List<Yield.Validator>,
) : ValidatorState()
data object Loading : ValidatorState()
data object Loading : ValidatorState() {
override val isClickable: Boolean
get() = false
}
data object Error : ValidatorState()
data object Error : ValidatorState() {
override val isClickable: Boolean
get() = false
}
fun copySealed(isClickable: Boolean): ValidatorState {
return if (this is Content) {
copy(isClickable = isClickable)
} else {
this
}
}
}

View file

@ -12,7 +12,7 @@ import com.tangem.features.staking.impl.presentation.state.ValidatorState
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
internal object ConfirmStakingStatePreviewData {
internal object ConfirmationStatePreviewData {
private val validatorList = listOf(
Yield.Validator(
@ -62,8 +62,9 @@ internal object ConfirmStakingStatePreviewData {
),
)
val confirmStakingState = StakingStates.ConfirmStakingState.Data(
val assentStakingState = StakingStates.ConfirmationState.Data(
isPrimaryButtonEnabled = true,
innerState = InnerConfirmationStakingState.ASSENT,
feeState = FeeState.Content(
fee = fee,
rate = BigDecimal.ONE,
@ -72,6 +73,7 @@ internal object ConfirmStakingStatePreviewData {
isFeeConvertibleToFiat = true,
),
validatorState = ValidatorState.Content(
isClickable = true,
chosenValidator = validatorList[0],
availableValidators = validatorList,
),
@ -82,6 +84,6 @@ internal object ConfirmStakingStatePreviewData {
days = 2,
),
),
innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM,
transactionDoneState = TransactionDoneState.Empty,
)
}

View file

@ -31,4 +31,8 @@ object StakingClickIntentsStub : StakingClickIntents {
override fun openRewardsValidators() {}
override fun selectRewardValidator(rewardValue: String) {}
override fun onExploreClick() {}
override fun onShareClick() {}
}

View file

@ -1,25 +0,0 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class SetConfirmStateInProgressTransformer : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
confirmStakingState = prevState.confirmStakingState.copyWrapped(),
)
}
private fun StakingStates.ConfirmStakingState.copyWrapped(): StakingStates.ConfirmStakingState {
return if (this is StakingStates.ConfirmStakingState.Data) {
copy(
isPrimaryButtonEnabled = false,
innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.IN_PROGRESS,
)
} else {
this
}
}
}

View file

@ -10,9 +10,10 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.Provider
import com.tangem.utils.transformer.Transformer
import com.tangem.blockchain.common.Amount
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
@Suppress("UnusedPrivateMember")
internal class SetConfirmStateConfirmTransformer(
internal class SetConfirmationStateAssentTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val stakingGasEstimate: StakingGasEstimate,
@ -20,15 +21,16 @@ internal class SetConfirmStateConfirmTransformer(
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
confirmStakingState = prevState.confirmStakingState.copyWrapped(stakingGasEstimate),
confirmationState = prevState.confirmationState.copyWrapped(stakingGasEstimate),
)
}
private fun StakingStates.ConfirmStakingState.copyWrapped(
private fun StakingStates.ConfirmationState.copyWrapped(
gasEstimate: StakingGasEstimate,
): StakingStates.ConfirmStakingState {
if (this is StakingStates.ConfirmStakingState.Data) {
): StakingStates.ConfirmationState {
if (this is StakingStates.ConfirmationState.Data) {
return copy(
innerState = InnerConfirmationStakingState.ASSENT,
feeState = FeeState.Content(
fee = Fee.Common(
Amount(
@ -42,8 +44,8 @@ internal class SetConfirmStateConfirmTransformer(
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
),
validatorState = validatorState.copySealed(isClickable = true),
isPrimaryButtonEnabled = true,
innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM,
)
} else {
return this

View file

@ -0,0 +1,60 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.transaction.StakingGasEstimate
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.*
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.TransactionDoneState
import com.tangem.utils.Provider
import com.tangem.utils.transformer.Transformer
internal class SetConfirmationStateCompletedTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val stakingGasEstimate: StakingGasEstimate,
private val txUrl: String,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
confirmationState = prevState.confirmationState.copyWrapped(stakingGasEstimate),
)
}
private fun StakingStates.ConfirmationState.copyWrapped(
gasEstimate: StakingGasEstimate,
): StakingStates.ConfirmationState {
if (this is StakingStates.ConfirmationState.Data) {
return copy(
isPrimaryButtonEnabled = true,
innerState = InnerConfirmationStakingState.COMPLETED,
feeState = FeeState.Content(
fee = Fee.Common(
Amount(
currencySymbol = gasEstimate.token.symbol,
value = gasEstimate.amount,
decimals = gasEstimate.token.decimals,
),
),
rate = cryptoCurrencyStatusProvider().value.fiatRate,
isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate,
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
),
validatorState = validatorState.copySealed(
isClickable = false,
),
transactionDoneState = TransactionDoneState.Content(
timestamp = System.currentTimeMillis(),
txUrl = txUrl,
),
)
} else {
return this
}
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class SetConfirmationStateInProgressTransformer : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
confirmationState = prevState.confirmationState.copyWrapped(),
)
}
private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState {
return if (this is StakingStates.ConfirmationState.Data) {
copy(
isPrimaryButtonEnabled = false,
innerState = InnerConfirmationStakingState.IN_PROGRESS,
validatorState = validatorState.copySealed(isClickable = false),
)
} else {
this
}
}
}

View file

@ -10,20 +10,22 @@ import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
@Suppress("UnusedPrivateMember")
internal class SetConfirmStateLoadingTransformer(
internal class SetConfirmationStateLoadingTransformer(
private val yield: Yield,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val possibleConfirmStakingState = prevState.confirmStakingState as? StakingStates.ConfirmStakingState.Data
val possibleValidatorState = possibleConfirmStakingState?.validatorState as? ValidatorState.Content
val possibleConfirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
val possibleValidatorState = possibleConfirmationState?.validatorState as? ValidatorState.Content
val chosenValidator = possibleValidatorState?.chosenValidator ?: yield.validators[0]
return prevState.copy(
confirmStakingState = StakingStates.ConfirmStakingState.Data(
confirmationState = StakingStates.ConfirmationState.Data(
isPrimaryButtonEnabled = false,
innerState = InnerConfirmationStakingState.ASSENT,
feeState = FeeState.Loading,
validatorState = ValidatorState.Content(
isClickable = false,
chosenValidator = chosenValidator,
availableValidators = yield.validators,
),
@ -34,7 +36,7 @@ internal class SetConfirmStateLoadingTransformer(
),
),
footerText = "",
innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM,
transactionDoneState = TransactionDoneState.Empty,
),
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.common.extensions.remove
import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
@ -20,7 +21,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter
import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.extensions.orZero
@ -61,7 +62,7 @@ internal class SetInitialDataStateTransformer(
currentStep = StakingStep.InitialInfo,
initialInfoState = createInitialInfoState(),
amountState = createInitialAmountState(),
confirmStakingState = createInitialConfirmationState(),
confirmationState = createInitialConfirmationState(),
rewardsValidatorsState = rewardsValidatorStateConverter.convert(Unit),
bottomSheetConfig = null,
)
@ -98,9 +99,10 @@ internal class SetInitialDataStateTransformer(
return amountStateConverter.convert("")
}
private fun createInitialConfirmationState(): StakingStates.ConfirmStakingState {
return ConfirmStakingStatePreviewData.confirmStakingState.copy(
private fun createInitialConfirmationState(): StakingStates.ConfirmationState {
return ConfirmationStatePreviewData.assentStakingState.copy(
validatorState = ValidatorState.Content(
isClickable = true,
chosenValidator = yield.validators.first(),
availableValidators = yield.validators,
),
@ -116,7 +118,7 @@ internal class SetInitialDataStateTransformer(
val formattedMinApr = BigDecimalFormatter.formatPercent(
percent = minApr,
useAbsoluteValue = true,
)
).remove("%")
val formattedMaxApr = BigDecimalFormatter.formatPercent(
percent = maxApr,
useAbsoluteValue = true,

View file

@ -10,11 +10,11 @@ internal class ValidatorSelectChangeTransformer(
private val selectedValidator: Yield.Validator,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val confirmState = prevState.confirmStakingState as? StakingStates.ConfirmStakingState.Data ?: return prevState
val validatorState = confirmState.validatorState as? ValidatorState.Content ?: return prevState
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: return prevState
return prevState.copy(
confirmStakingState = confirmState.copy(
confirmationState = confirmationState.copy(
validatorState = validatorState.copy(
chosenValidator = selectedValidator,
),

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@ -14,23 +15,26 @@ 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.components.transactions.TransactionDoneTitle
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.state.previewdata.ConfirmationStatePreviewData
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock
import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock
import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.TransactionDoneState
@Composable
internal fun StakingConfirmContent(
internal fun StakingConfirmationContent(
amountState: AmountState,
state: StakingStates.ConfirmStakingState,
state: StakingStates.ConfirmationState,
clickIntents: StakingClickIntents,
) {
if (state !is StakingStates.ConfirmStakingState.Data) return
if (state !is StakingStates.ConfirmationState.Data) return
Column(
modifier = Modifier
@ -40,6 +44,16 @@ internal fun StakingConfirmContent(
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
AnimatedVisibility(
visible = state.transactionDoneState is TransactionDoneState.Content,
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
) {
val transactionDoneStateContent = state.transactionDoneState as TransactionDoneState.Content
TransactionDoneTitle(
titleRes = R.string.sent_transaction_sent_title,
date = transactionDoneStateContent.timestamp,
)
}
AmountBlock(
amountState = amountState,
isClickDisabled = true,
@ -68,12 +82,12 @@ private fun FooterText(text: String) {
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_StakingConfirmContent() {
private fun Preview_StakingConfirmationContent() {
TangemThemePreview {
Column(Modifier.background(TangemTheme.colors.background.primary)) {
StakingConfirmContent(
StakingConfirmationContent(
amountState = AmountStatePreviewData.amountState,
state = ConfirmStakingStatePreviewData.confirmStakingState,
state = ConfirmationStatePreviewData.assentStakingState,
clickIntents = StakingClickIntentsStub,
)
}

View file

@ -17,19 +17,22 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.tangem.common.ui.amountScreen.ui.SendDoneButtons
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingStep
import com.tangem.features.staking.impl.presentation.state.StakingUiState
@Composable
internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifier = Modifier) {
val confirmInnerState = (uiState.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState
val isSuccessState = confirmInnerState == InnerConfirmationStakingState.COMPLETED
Column(
modifier = modifier
.padding(
@ -38,6 +41,15 @@ internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifie
bottom = TangemTheme.dimens.spacing16,
),
) {
val confirmationDataState = uiState.confirmationState as? StakingStates.ConfirmationState.Data
val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content
SendDoneButtons(
txUrl = transactionDoneState?.txUrl.orEmpty(),
onExploreClick = uiState.clickIntents::onExploreClick,
onShareClick = uiState.clickIntents::onShareClick,
isVisible = isSuccessState,
)
StakingNavigationButton(
uiState = uiState,
modifier = Modifier,
@ -50,13 +62,19 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier
val hapticFeedback = LocalHapticFeedback.current
val isButtonsVisible = isPrevButtonVisible(uiState.currentStep)
val isStakingState = uiState.currentStep == StakingStep.Confirm
val innerConfirmState = (uiState.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState
val isInProgressInnerState = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS
val isInAssentInnerState = innerConfirmState == InnerConfirmationStakingState.ASSENT
val showTangemIcon = uiState.currentStep == StakingStep.Confirmation &&
(isInProgressInnerState || isInAssentInnerState)
val (buttonTextId, buttonClick) = getButtonData(
currentState = uiState,
)
val isButtonEnabled = isButtonEnabled(uiState)
val buttonIcon = if (isStakingState) {
val buttonIcon = if (showTangemIcon) {
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
} else {
TangemButtonIconPosition.None
@ -82,7 +100,6 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier
SpacerW12()
}
}
AnimatedVisibility(
visible = buttonClick != null,
enter = fadeIn(),
@ -93,7 +110,7 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier
icon = buttonIcon,
enabled = isButtonEnabled && buttonClick != null,
onClick = {
if (isStakingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
if (showTangemIcon) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
if (buttonClick != null) buttonClick()
},
showProgress = false,
@ -116,18 +133,27 @@ private fun getButtonData(currentState: StakingUiState): Pair<Int, (() -> Unit)?
}
StakingStep.Amount,
-> R.string.common_next to currentState.clickIntents::onNextClick
StakingStep.Confirm -> R.string.common_stake to currentState.clickIntents::onNextClick
StakingStep.Confirmation -> {
val confirmationState = currentState.confirmationState
if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
R.string.common_close to currentState.clickIntents::onBackClick
} else {
R.string.common_stake to currentState.clickIntents::onNextClick
}
} else {
R.string.common_close to currentState.clickIntents::onBackClick
}
}
StakingStep.Validators -> R.string.common_continue to currentState.clickIntents::onNextClick
StakingStep.Success -> R.string.common_close to currentState.clickIntents::onBackClick
else -> R.string.common_next to null
StakingStep.RewardsValidators -> R.string.common_next to null
}
}
private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) {
StakingStep.InitialInfo,
StakingStep.RewardsValidators,
StakingStep.Confirm,
StakingStep.Success,
StakingStep.Confirmation,
-> false
StakingStep.Amount,
StakingStep.Validators,
@ -138,9 +164,8 @@ private fun isButtonEnabled(uiState: StakingUiState): Boolean {
return when (uiState.currentStep) {
StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled
StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled
StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled
StakingStep.Confirmation -> uiState.confirmationState.isPrimaryButtonEnabled
StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled
StakingStep.Success -> true
StakingStep.Validators -> true
}
}

View file

@ -67,15 +67,13 @@ private fun SendAppBar(uiState: StakingUiState) {
StakingStep.InitialInfo,
StakingStep.RewardsValidators,
StakingStep.Validators,
StakingStep.Confirm,
StakingStep.Confirmation,
-> stringResource(id = R.string.common_stake)
StakingStep.Success -> ""
}
val backIcon = when (uiState.currentStep) {
StakingStep.Amount,
StakingStep.Validators,
StakingStep.Confirm,
StakingStep.Success,
StakingStep.Confirmation,
-> {
R.drawable.ic_close_24
}
@ -153,20 +151,19 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M
isBalanceHiding = uiState.isBalanceHidden,
clickIntents = uiState.clickIntents,
)
StakingStep.Confirm -> StakingConfirmContent(
StakingStep.Confirmation -> StakingConfirmationContent(
amountState = uiState.amountState,
state = uiState.confirmStakingState,
state = uiState.confirmationState,
clickIntents = uiState.clickIntents,
)
StakingStep.Validators -> {
val confirmState = uiState.confirmStakingState
if (confirmState !is StakingStates.ConfirmStakingState.Data) return@AnimatedContent
val confirmState = uiState.confirmationState
if (confirmState !is StakingStates.ConfirmationState.Data) return@AnimatedContent
StakingValidatorListContent(
state = confirmState.validatorState,
clickIntents = uiState.clickIntents,
)
}
else -> TODO()
}
}
}

View file

@ -27,7 +27,7 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import java.math.BigDecimal
@ -124,6 +124,6 @@ private fun StakingValidatorListContent_Preview(
private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider<ValidatorState> {
override val values: Sequence<ValidatorState>
get() = sequenceOf(ConfirmStakingStatePreviewData.confirmStakingState.validatorState)
get() = sequenceOf(ConfirmationStatePreviewData.assentStakingState.validatorState)
}
// endregion

View file

@ -35,6 +35,7 @@ internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(
enabled = validatorState.isClickable,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
onClick = onClick,
@ -63,6 +64,7 @@ internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit)
),
),
imageUrl = validatorState.chosenValidator.image.orEmpty(),
showChevron = validatorState.isClickable,
)
}
}

View file

@ -23,4 +23,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun openRewardsValidators()
fun selectRewardValidator(rewardValue: String)
fun onExploreClick()
fun onShareClick()
}

View file

@ -21,6 +21,7 @@ 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.SendTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
@ -56,6 +57,7 @@ internal class StakingViewModel @Inject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val initializeStakingProcessUseCase: InitializeStakingProcessUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents {
@ -97,18 +99,15 @@ internal class StakingViewModel @Inject constructor(
}
override fun onNextClick() {
handleOnNextConfirmClick()
handleOnNextConfirmationClick()
stakingStateRouter.onNextClick()
if (value.currentStep == StakingStep.Confirm) {
if (isAssentState()) {
initStaking()
}
}
private fun handleOnNextConfirmClick() {
if (value.currentStep == StakingStep.Confirm &&
(value.confirmStakingState as? StakingStates.ConfirmStakingState.Data)?.innerState ==
StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM
) {
private fun handleOnNextConfirmationClick() {
if (isAssentState()) {
viewModelScope.launch {
stakingTransaction?.unsignedTransaction?.let {
sendStakingTransaction(TransactionData.Compiled(value = it.hexToBytes()))
@ -120,7 +119,7 @@ internal class StakingViewModel @Inject constructor(
private fun initStaking() {
viewModelScope.launch {
stateController.update(
SetConfirmStateLoadingTransformer(
SetConfirmationStateLoadingTransformer(
yield = yield,
),
)
@ -144,7 +143,7 @@ internal class StakingViewModel @Inject constructor(
val stakingGasEstimate = stakingTransaction.gasEstimate ?: error("Can't get fee info")
stateController.update(
SetConfirmStateConfirmTransformer(
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = stakingGasEstimate,
@ -196,6 +195,20 @@ internal class StakingViewModel @Inject constructor(
stakingStateRouter.onNextClick()
}
override fun onExploreClick() {
val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data
val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content
val txUrl = transactionDoneState?.txUrl
if (txUrl != null) {
innerRouter.openUrl(txUrl)
}
}
override fun onShareClick() {
// TODO staking analytics event
}
fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) {
innerRouter = router
this.stakingStateRouter = stateRouter
@ -254,7 +267,7 @@ internal class StakingViewModel @Inject constructor(
}
private suspend fun sendStakingTransaction(txData: TransactionData) {
stateController.update(SetConfirmStateInProgressTransformer())
stateController.update(SetConfirmationStateInProgressTransformer())
sendTransactionUseCase(
txData = txData,
@ -265,29 +278,35 @@ internal class StakingViewModel @Inject constructor(
Timber.e(error.toString())
val gasEstimate = stakingTransaction?.gasEstimate ?: return@fold
stateController.update(
SetConfirmStateConfirmTransformer(
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = gasEstimate,
),
)
// uiState = eventStateFactory.getSendTransactionErrorState(
// error = error,
// onConsume = { uiState = eventStateFactory.onConsumeEventState() },
// )
// analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol))
},
ifRight = {
ifRight = { txHash ->
val gasEstimate = stakingTransaction?.gasEstimate ?: return@fold
val txUrl = getExplorerTransactionUrlUseCase(
txHash = txHash,
networkId = cryptoCurrencyStatus.currency.network.id,
).getOrElse { "" }
stateController.update(
SetConfirmStateConfirmTransformer(
SetConfirmationStateCompletedTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = gasEstimate,
txUrl = txUrl,
),
)
// sendScreenAnalyticSender.sendTransaction()
},
)
}
private fun isAssentState(): Boolean {
return value.currentStep == StakingStep.Confirmation &&
(value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState ==
InnerConfirmationStakingState.ASSENT
}
}