diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemHoldToConfirmButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemHoldToConfirmButton.kt index 1eabf50efc..3ee514559a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemHoldToConfirmButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemHoldToConfirmButton.kt @@ -74,13 +74,14 @@ private const val DEFAULT_SCALE = 1f private const val SHAKE_AMPLITUDE_DP = 5f 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 HAPTIC_INITIAL_INTERVAL_MS = 200L -private const val HAPTIC_MIN_INTERVAL_MS = 30L +private const val HAPTIC_INITIAL_INTERVAL_MS = 50L +private const val HAPTIC_MIN_INTERVAL_MS = 15L private const val HAPTIC_HEARTBEAT_INTERVAL_MS = 100L +private const val HAPTIC_SUCCESS_DELAY_MS = 300L // Easing: smooth acceleration, gradually picking up speed private val AccelerateEasing = CubicBezierEasing( @@ -288,6 +289,7 @@ private fun Modifier.holdToConfirmGestures( state.isProgressVisible = true // Soft heartbeat on success + delay(HAPTIC_SUCCESS_DELAY_MS) performSuccessHapticFeedback(config.hapticManager) state.scaleProgress.animateTo( @@ -324,7 +326,7 @@ private suspend fun performAcceleratingHapticFeedback(hapticManager: HapticManag // Exponential decrease: interval decreases faster as progress increases val interval = (maxInterval * (1f - progress * progress) + minInterval).toLong() - hapticManager.perform(TangemHapticEffect.View.SegmentTick) + hapticManager.perform(TangemHapticEffect.View.ClockTick) delay(interval) } } @@ -333,18 +335,18 @@ private suspend fun performAcceleratingHapticFeedback(hapticManager: HapticManag * Performs strong heartbeat haptic feedback on release (two heavy clicks). */ private suspend fun performReleaseHapticFeedback(hapticManager: HapticManager) { - hapticManager.perform(TangemHapticEffect.OneTime.HeavyClick) + hapticManager.perform(TangemHapticEffect.View.Reject) 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). */ private suspend fun performSuccessHapticFeedback(hapticManager: HapticManager) { - hapticManager.perform(TangemHapticEffect.OneTime.Click) + hapticManager.perform(TangemHapticEffect.View.Confirm) delay(HAPTIC_HEARTBEAT_INTERVAL_MS) - hapticManager.perform(TangemHapticEffect.OneTime.Click) + hapticManager.perform(TangemHapticEffect.View.Confirm) } private suspend fun CoroutineScope.handleReleaseAnimation( diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt index 5df91b67b1..976b735b4f 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt @@ -110,4 +110,5 @@ val UserWallet.isLocked is UserWallet.Hot -> isLocked } -inline val UserWallet.isHotWallet get() = this is UserWallet.Hot \ No newline at end of file +inline val UserWallet.isHotWallet get() = this is UserWallet.Hot +inline val UserWallet.isColdWallet get() = this is UserWallet.Cold \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index a9164b0dca..e8ee07c6e8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -18,6 +18,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update 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 @ModelScoped @@ -35,9 +37,10 @@ internal class StakingStateController @Inject constructor( private val titleTransformer = SetTitleTransformer fun initializeWithUserWallet(userWallet: UserWallet) { - mutableUiState.update { - it.copy( - showColdWalletInteractionIcon = userWallet is UserWallet.Cold, + mutableUiState.update { state -> + state.copy( + showColdWalletInteractionIcon = userWallet.isColdWallet, + shouldShowHoldToConfirmButton = userWallet.isHotWallet, ) } } @@ -98,6 +101,7 @@ internal class StakingStateController @Inject constructor( buttonsState = NavigationButtonsState.Empty, balanceState = null, showColdWalletInteractionIcon = true, + shouldShowHoldToConfirmButton = false, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 1b6605e4ff..699729948f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -43,6 +43,7 @@ internal data class StakingUiState( val event: StateEvent, val balanceState: BalanceState?, val showColdWalletInteractionIcon: Boolean, + val shouldShowHoldToConfirmButton: Boolean, ) { fun copyWrapped( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 05f544c7bb..04a86dbec7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -7,6 +7,7 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference 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.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle @@ -45,15 +46,17 @@ internal class SetButtonsStateTransformer( val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED - val isIconVisible = isConfirmation && !isCompleted + val isHoldToConfirm = prevState.shouldShowHoldToConfirmButton && isConfirmation && !isCompleted + val isIconVisible = isConfirmation && !isCompleted && !isHoldToConfirm val isPrimaryButtonDisabled = prevState.isPrimaryButtonDisabled() return NavigationButton( - textReference = prevState.getButtonText(), + textReference = prevState.getButtonText(isHoldToConfirm), iconRes = R.drawable.ic_tangem_24.takeIf { prevState.showColdWalletInteractionIcon }, isDimmed = isPrimaryButtonDisabled, isIconVisible = isIconVisible, shouldShowProgress = isInProgress, isEnabled = prevState.isButtonEnabled(), + isHoldToConfirm = isHoldToConfirm, onClick = { if (isPrimaryButtonDisabled) { prevState.clickIntents.showPrimaryClickAlert() @@ -89,7 +92,7 @@ internal class SetButtonsStateTransformer( else -> true } - private fun StakingUiState.getButtonText(): TextReference { + private fun StakingUiState.getButtonText(isHoldToConfirm: Boolean): TextReference { return when (currentStep) { StakingStep.InitialInfo -> { val initialState = initialInfoState as? StakingStates.InitialInfoState.Data @@ -100,7 +103,7 @@ internal class SetButtonsStateTransformer( } } StakingStep.Success -> resourceReference(R.string.common_close) - StakingStep.Confirmation -> getConfirmationButtonText() + StakingStep.Confirmation -> getConfirmationButtonText(isHoldToConfirm) StakingStep.Validators -> resourceReference(R.string.common_continue) StakingStep.Amount, 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 + ?: return resourceReference(R.string.common_close) val amountState = amountState as? AmountState.Data - return if (confirmationState != null && amountState != null) { - when (actionType) { - is StakingActionCommonType.Enter -> { - val amount = amountState.amountTextField.cryptoAmount.value.orZero() - if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) { - 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() + ?: return resourceReference(R.string.common_close) + + if (actionType is StakingActionCommonType.Enter) { + val amount = amountState.amountTextField.cryptoAmount.value.orZero() + if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) { + return resourceReference(R.string.give_permission_title) } - } 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() {