Updated on 2026-08-14
This commit is contained in:
parent
f626613f80
commit
4962acd1ca
5 changed files with 54 additions and 31 deletions
|
|
@ -74,13 +74,14 @@ private const val DEFAULT_SCALE = 1f
|
||||||
|
|
||||||
private const val SHAKE_AMPLITUDE_DP = 5f
|
private const val SHAKE_AMPLITUDE_DP = 5f
|
||||||
private const val SHAKE_DURATION_MS = 120
|
private const val SHAKE_DURATION_MS = 120
|
||||||
private const val FADE_DURATION_MS = 150
|
private const val FADE_DURATION_MS = 100
|
||||||
|
|
||||||
private const val BOUNCE_VELOCITY_MULTIPLIER = 1.25f
|
private const val BOUNCE_VELOCITY_MULTIPLIER = 1.25f
|
||||||
|
|
||||||
private const val HAPTIC_INITIAL_INTERVAL_MS = 200L
|
private const val HAPTIC_INITIAL_INTERVAL_MS = 50L
|
||||||
private const val HAPTIC_MIN_INTERVAL_MS = 30L
|
private const val HAPTIC_MIN_INTERVAL_MS = 15L
|
||||||
private const val HAPTIC_HEARTBEAT_INTERVAL_MS = 100L
|
private const val HAPTIC_HEARTBEAT_INTERVAL_MS = 100L
|
||||||
|
private const val HAPTIC_SUCCESS_DELAY_MS = 300L
|
||||||
|
|
||||||
// Easing: smooth acceleration, gradually picking up speed
|
// Easing: smooth acceleration, gradually picking up speed
|
||||||
private val AccelerateEasing = CubicBezierEasing(
|
private val AccelerateEasing = CubicBezierEasing(
|
||||||
|
|
@ -288,6 +289,7 @@ private fun Modifier.holdToConfirmGestures(
|
||||||
state.isProgressVisible = true
|
state.isProgressVisible = true
|
||||||
|
|
||||||
// Soft heartbeat on success
|
// Soft heartbeat on success
|
||||||
|
delay(HAPTIC_SUCCESS_DELAY_MS)
|
||||||
performSuccessHapticFeedback(config.hapticManager)
|
performSuccessHapticFeedback(config.hapticManager)
|
||||||
|
|
||||||
state.scaleProgress.animateTo(
|
state.scaleProgress.animateTo(
|
||||||
|
|
@ -324,7 +326,7 @@ private suspend fun performAcceleratingHapticFeedback(hapticManager: HapticManag
|
||||||
// Exponential decrease: interval decreases faster as progress increases
|
// Exponential decrease: interval decreases faster as progress increases
|
||||||
val interval = (maxInterval * (1f - progress * progress) + minInterval).toLong()
|
val interval = (maxInterval * (1f - progress * progress) + minInterval).toLong()
|
||||||
|
|
||||||
hapticManager.perform(TangemHapticEffect.View.SegmentTick)
|
hapticManager.perform(TangemHapticEffect.View.ClockTick)
|
||||||
delay(interval)
|
delay(interval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -333,18 +335,18 @@ private suspend fun performAcceleratingHapticFeedback(hapticManager: HapticManag
|
||||||
* Performs strong heartbeat haptic feedback on release (two heavy clicks).
|
* Performs strong heartbeat haptic feedback on release (two heavy clicks).
|
||||||
*/
|
*/
|
||||||
private suspend fun performReleaseHapticFeedback(hapticManager: HapticManager) {
|
private suspend fun performReleaseHapticFeedback(hapticManager: HapticManager) {
|
||||||
hapticManager.perform(TangemHapticEffect.OneTime.HeavyClick)
|
hapticManager.perform(TangemHapticEffect.View.Reject)
|
||||||
delay(HAPTIC_HEARTBEAT_INTERVAL_MS)
|
delay(HAPTIC_HEARTBEAT_INTERVAL_MS)
|
||||||
hapticManager.perform(TangemHapticEffect.OneTime.HeavyClick)
|
hapticManager.perform(TangemHapticEffect.View.Reject)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs soft heartbeat haptic feedback on success (two light clicks).
|
* Performs soft heartbeat haptic feedback on success (two light clicks).
|
||||||
*/
|
*/
|
||||||
private suspend fun performSuccessHapticFeedback(hapticManager: HapticManager) {
|
private suspend fun performSuccessHapticFeedback(hapticManager: HapticManager) {
|
||||||
hapticManager.perform(TangemHapticEffect.OneTime.Click)
|
hapticManager.perform(TangemHapticEffect.View.Confirm)
|
||||||
delay(HAPTIC_HEARTBEAT_INTERVAL_MS)
|
delay(HAPTIC_HEARTBEAT_INTERVAL_MS)
|
||||||
hapticManager.perform(TangemHapticEffect.OneTime.Click)
|
hapticManager.perform(TangemHapticEffect.View.Confirm)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun CoroutineScope.handleReleaseAnimation(
|
private suspend fun CoroutineScope.handleReleaseAnimation(
|
||||||
|
|
|
||||||
|
|
@ -110,4 +110,5 @@ val UserWallet.isLocked
|
||||||
is UserWallet.Hot -> isLocked
|
is UserWallet.Hot -> isLocked
|
||||||
}
|
}
|
||||||
|
|
||||||
inline val UserWallet.isHotWallet get() = this is UserWallet.Hot
|
inline val UserWallet.isHotWallet get() = this is UserWallet.Hot
|
||||||
|
inline val UserWallet.isColdWallet get() = this is UserWallet.Cold
|
||||||
|
|
@ -18,6 +18,8 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
|
import com.tangem.domain.models.wallet.isColdWallet
|
||||||
|
import com.tangem.domain.models.wallet.isHotWallet
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ModelScoped
|
@ModelScoped
|
||||||
|
|
@ -35,9 +37,10 @@ internal class StakingStateController @Inject constructor(
|
||||||
private val titleTransformer = SetTitleTransformer
|
private val titleTransformer = SetTitleTransformer
|
||||||
|
|
||||||
fun initializeWithUserWallet(userWallet: UserWallet) {
|
fun initializeWithUserWallet(userWallet: UserWallet) {
|
||||||
mutableUiState.update {
|
mutableUiState.update { state ->
|
||||||
it.copy(
|
state.copy(
|
||||||
showColdWalletInteractionIcon = userWallet is UserWallet.Cold,
|
showColdWalletInteractionIcon = userWallet.isColdWallet,
|
||||||
|
shouldShowHoldToConfirmButton = userWallet.isHotWallet,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -98,6 +101,7 @@ internal class StakingStateController @Inject constructor(
|
||||||
buttonsState = NavigationButtonsState.Empty,
|
buttonsState = NavigationButtonsState.Empty,
|
||||||
balanceState = null,
|
balanceState = null,
|
||||||
showColdWalletInteractionIcon = true,
|
showColdWalletInteractionIcon = true,
|
||||||
|
shouldShowHoldToConfirmButton = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +43,7 @@ internal data class StakingUiState(
|
||||||
val event: StateEvent<StakingEvent>,
|
val event: StateEvent<StakingEvent>,
|
||||||
val balanceState: BalanceState?,
|
val balanceState: BalanceState?,
|
||||||
val showColdWalletInteractionIcon: Boolean,
|
val showColdWalletInteractionIcon: Boolean,
|
||||||
|
val shouldShowHoldToConfirmButton: Boolean,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun copyWrapped(
|
fun copyWrapped(
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import com.tangem.core.navigation.url.UrlOpener
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||||
import com.tangem.features.staking.impl.presentation.state.*
|
import com.tangem.features.staking.impl.presentation.state.*
|
||||||
import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle
|
import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle
|
||||||
|
|
@ -45,15 +46,17 @@ internal class SetButtonsStateTransformer(
|
||||||
val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS
|
val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS
|
||||||
val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED
|
val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED
|
||||||
|
|
||||||
val isIconVisible = isConfirmation && !isCompleted
|
val isHoldToConfirm = prevState.shouldShowHoldToConfirmButton && isConfirmation && !isCompleted
|
||||||
|
val isIconVisible = isConfirmation && !isCompleted && !isHoldToConfirm
|
||||||
val isPrimaryButtonDisabled = prevState.isPrimaryButtonDisabled()
|
val isPrimaryButtonDisabled = prevState.isPrimaryButtonDisabled()
|
||||||
return NavigationButton(
|
return NavigationButton(
|
||||||
textReference = prevState.getButtonText(),
|
textReference = prevState.getButtonText(isHoldToConfirm),
|
||||||
iconRes = R.drawable.ic_tangem_24.takeIf { prevState.showColdWalletInteractionIcon },
|
iconRes = R.drawable.ic_tangem_24.takeIf { prevState.showColdWalletInteractionIcon },
|
||||||
isDimmed = isPrimaryButtonDisabled,
|
isDimmed = isPrimaryButtonDisabled,
|
||||||
isIconVisible = isIconVisible,
|
isIconVisible = isIconVisible,
|
||||||
shouldShowProgress = isInProgress,
|
shouldShowProgress = isInProgress,
|
||||||
isEnabled = prevState.isButtonEnabled(),
|
isEnabled = prevState.isButtonEnabled(),
|
||||||
|
isHoldToConfirm = isHoldToConfirm,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (isPrimaryButtonDisabled) {
|
if (isPrimaryButtonDisabled) {
|
||||||
prevState.clickIntents.showPrimaryClickAlert()
|
prevState.clickIntents.showPrimaryClickAlert()
|
||||||
|
|
@ -89,7 +92,7 @@ internal class SetButtonsStateTransformer(
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun StakingUiState.getButtonText(): TextReference {
|
private fun StakingUiState.getButtonText(isHoldToConfirm: Boolean): TextReference {
|
||||||
return when (currentStep) {
|
return when (currentStep) {
|
||||||
StakingStep.InitialInfo -> {
|
StakingStep.InitialInfo -> {
|
||||||
val initialState = initialInfoState as? StakingStates.InitialInfoState.Data
|
val initialState = initialInfoState as? StakingStates.InitialInfoState.Data
|
||||||
|
|
@ -100,7 +103,7 @@ internal class SetButtonsStateTransformer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StakingStep.Success -> resourceReference(R.string.common_close)
|
StakingStep.Success -> resourceReference(R.string.common_close)
|
||||||
StakingStep.Confirmation -> getConfirmationButtonText()
|
StakingStep.Confirmation -> getConfirmationButtonText(isHoldToConfirm)
|
||||||
StakingStep.Validators -> resourceReference(R.string.common_continue)
|
StakingStep.Validators -> resourceReference(R.string.common_continue)
|
||||||
StakingStep.Amount,
|
StakingStep.Amount,
|
||||||
StakingStep.RestakeValidator,
|
StakingStep.RestakeValidator,
|
||||||
|
|
@ -109,25 +112,37 @@ internal class SetButtonsStateTransformer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun StakingUiState.getConfirmationButtonText(): TextReference {
|
private fun StakingUiState.getConfirmationButtonText(isHoldToConfirm: Boolean): TextReference {
|
||||||
val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data
|
val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data
|
||||||
|
?: return resourceReference(R.string.common_close)
|
||||||
val amountState = amountState as? AmountState.Data
|
val amountState = amountState as? AmountState.Data
|
||||||
return if (confirmationState != null && amountState != null) {
|
?: return resourceReference(R.string.common_close)
|
||||||
when (actionType) {
|
|
||||||
is StakingActionCommonType.Enter -> {
|
if (actionType is StakingActionCommonType.Enter) {
|
||||||
val amount = amountState.amountTextField.cryptoAmount.value.orZero()
|
val amount = amountState.amountTextField.cryptoAmount.value.orZero()
|
||||||
if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) {
|
if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) {
|
||||||
resourceReference(R.string.give_permission_title)
|
return resourceReference(R.string.give_permission_title)
|
||||||
} else {
|
|
||||||
resourceReference(R.string.common_stake)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake)
|
|
||||||
is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle()
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
resourceReference(R.string.common_close)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val baseText = getBaseActionText(confirmationState)
|
||||||
|
?: return resourceReference(R.string.common_close)
|
||||||
|
|
||||||
|
return baseText.wrapWithHoldToIf(isHoldToConfirm)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StakingUiState.getBaseActionText(
|
||||||
|
confirmationState: StakingStates.ConfirmationState.Data,
|
||||||
|
): TextReference? = when (actionType) {
|
||||||
|
is StakingActionCommonType.Enter -> resourceReference(R.string.common_stake)
|
||||||
|
is StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake)
|
||||||
|
is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TextReference.wrapWithHoldToIf(condition: Boolean): TextReference = if (condition) {
|
||||||
|
resourceReference(id = R.string.common_hold_to, formatArgs = wrappedList(this))
|
||||||
|
} else {
|
||||||
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun StakingUiState.onPrimaryClick() {
|
private fun StakingUiState.onPrimaryClick() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue