mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-25 18:11:42 -04:00
Add haptic feedback to mobile app primary actions for improved UX (#1802)
This commit is contained in:
committed by
Leendert de Borst
parent
608da07bc4
commit
381fe65546
@@ -446,6 +446,9 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
private suspend fun handlePinResult(result: PinResult) {
|
||||
when (result) {
|
||||
is PinResult.Success -> {
|
||||
// Trigger success haptic feedback
|
||||
triggerSuccessFeedback()
|
||||
|
||||
// Success - return result
|
||||
val resultIntent = Intent()
|
||||
result.encryptionKey?.let {
|
||||
@@ -532,6 +535,18 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun triggerSuccessFeedback() {
|
||||
// Trigger haptic feedback for success
|
||||
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
// Use a lighter, shorter vibration for success (50ms)
|
||||
vibrator?.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrator?.vibrate(50)
|
||||
}
|
||||
}
|
||||
|
||||
private fun triggerErrorFeedback() {
|
||||
// Trigger haptic feedback for error
|
||||
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||
|
||||
@@ -914,6 +914,11 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
setIsSaving(false);
|
||||
setIsSaveDisabled(false);
|
||||
|
||||
// Haptic feedback for successful save
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
|
||||
// Navigate immediately - sync continues in background
|
||||
if (itemUrl && !isEditMode) {
|
||||
router.replace('/items/autofill-item-created');
|
||||
@@ -976,6 +981,11 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
|
||||
emitter.emit('credentialChanged', id);
|
||||
|
||||
// Haptic feedback for delete action (warning type for destructive action)
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { router } from 'expo-router';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
@@ -122,6 +123,11 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
return;
|
||||
}
|
||||
|
||||
// Haptic feedback for successful unlock
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
|
||||
router.replace('/reinitialize');
|
||||
} catch (err) {
|
||||
if (err instanceof VaultVersionIncompatibleError) {
|
||||
@@ -132,6 +138,11 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
console.error('Unlock error:', err);
|
||||
const errorCode = getAppErrorCode(err);
|
||||
|
||||
// Haptic feedback for authentication error
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
}
|
||||
|
||||
if (!errorCode || errorCode === AppErrorCode.VAULT_DECRYPT_FAILED) {
|
||||
setError(t('auth.errors.incorrectPassword'));
|
||||
} else {
|
||||
@@ -192,6 +203,11 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
return;
|
||||
}
|
||||
|
||||
// Haptic feedback for successful unlock
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
|
||||
/*
|
||||
* Navigate to reinitialize which will sync vault with server
|
||||
* and then navigate to the appropriate destination.
|
||||
@@ -207,6 +223,11 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
// Try to extract error code from the error
|
||||
const errorCode = getAppErrorCode(err);
|
||||
|
||||
// Haptic feedback for authentication error
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
}
|
||||
|
||||
/*
|
||||
* During unlock, VAULT_DECRYPT_FAILED indicates wrong password.
|
||||
* This is thrown when decryption fails due to incorrect encryption key.
|
||||
@@ -240,6 +261,12 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
router.replace('/upgrade');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Haptic feedback for successful unlock
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
|
||||
router.replace('/reinitialize');
|
||||
return true;
|
||||
}
|
||||
@@ -272,6 +299,12 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
router.replace('/upgrade');
|
||||
return;
|
||||
}
|
||||
|
||||
// Haptic feedback for successful unlock
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
|
||||
router.replace('/reinitialize');
|
||||
} catch (err) {
|
||||
console.error('Biometric retry error:', err);
|
||||
@@ -287,6 +320,11 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
if (pinAvailable) {
|
||||
await performPinUnlock();
|
||||
} else if (errorCode) {
|
||||
// Haptic feedback for authentication error
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
}
|
||||
|
||||
// Show the error with code if no PIN fallback
|
||||
const translationKey = getErrorTranslationKey(errorCode);
|
||||
setError(formatErrorWithCode(t(translationKey), errorCode));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -58,6 +60,12 @@ export const FolderModal: React.FC<IFolderModalProps> = ({
|
||||
|
||||
try {
|
||||
await onSave(trimmedName);
|
||||
|
||||
// Haptic feedback for successful folder creation/edit
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setError(t('common.errors.unknownErrorTryAgain'));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import React, { forwardRef, useImperativeHandle, useMemo, useRef, useState, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { View, TextInput, TextInputProps, StyleSheet, TouchableOpacity, Platform, ScrollView, Switch } from 'react-native';
|
||||
@@ -142,6 +143,11 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
if (password) {
|
||||
onChangeText(password);
|
||||
setShowPasswordState(true);
|
||||
|
||||
// Haptic feedback for password generation
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [currentSettings, generatePassword, onChangeText, setShowPasswordState]);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, Platform, Animated, Easing } from 'react-native';
|
||||
@@ -105,6 +106,11 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
// Use centralized clipboard utility
|
||||
await copyToClipboardWithExpiration(value, timeoutSeconds);
|
||||
|
||||
// Haptic feedback for successful copy
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
|
||||
// Handle animation state
|
||||
if (timeoutSeconds > 0) {
|
||||
// Clear any existing active field and set this one as active
|
||||
|
||||
@@ -282,6 +282,11 @@ public class PinUnlockViewModel: ObservableObject {
|
||||
// Call the injected unlock handler with the PIN
|
||||
// This will perform Argon2 key derivation which may take 500ms-1s
|
||||
try await unlockHandler(pin)
|
||||
|
||||
// Success - trigger success haptic feedback
|
||||
let generator = UINotificationFeedbackGenerator()
|
||||
generator.notificationOccurred(.success)
|
||||
|
||||
// Success - the handler will navigate away or complete the flow
|
||||
// Keep loading state active since we're navigating
|
||||
} catch let pinError as PinUnlockError {
|
||||
|
||||
Reference in New Issue
Block a user