mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-27 11:03:16 -04:00
Refactor haptics usage to shared utility (#1812)
This commit is contained in:
committed by
Leendert de Borst
parent
ec4aba9967
commit
ae8c995d16
@@ -1,14 +1,14 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useNavigation } from 'expo-router';
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, ScrollView, RefreshControl, Animated , Platform } from 'react-native';
|
||||
import { StyleSheet, Platform, View, ScrollView, RefreshControl, Animated } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import type { MailboxBulkRequest, MailboxBulkResponse, MailboxEmail } from '@/utils/dist/core/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
@@ -145,11 +145,7 @@ export default function EmailsScreen() : React.ReactNode {
|
||||
*/
|
||||
const onRefresh = useCallback(async () : Promise<void> => {
|
||||
// Trigger haptic feedback when pull-to-refresh is activated
|
||||
if (Platform.OS === 'ios') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
} else if (Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
|
||||
setIsLoading(true);
|
||||
setIsRefreshing(true);
|
||||
|
||||
@@ -9,6 +9,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, Keyboard, Platform, ScrollView, KeyboardAvoidingView, TouchableOpacity } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import type { Folder } from '@/utils/db/repositories/FolderRepository';
|
||||
import { CreateIdentityGenerator, CreateUsernameEmailGenerator, UsernameEmailGenerator, Gender, Identity, IdentityHelperUtils, convertAgeRangeToBirthdateOptions } from '@/utils/dist/core/identity-generator';
|
||||
import type { Attachment, Item, ItemField, TotpCode, ItemType, FieldType, PasswordSettings } from '@/utils/dist/core/models/vault';
|
||||
@@ -671,9 +673,7 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
Fields: []
|
||||
});
|
||||
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
}, [item, isEditMode]);
|
||||
|
||||
/**
|
||||
@@ -915,9 +915,7 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
setIsSaveDisabled(false);
|
||||
|
||||
// Haptic feedback for successful save
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
// Navigate immediately - sync continues in background
|
||||
if (itemUrl && !isEditMode) {
|
||||
@@ -982,9 +980,7 @@ 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);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Warning);
|
||||
|
||||
setTimeout(() => {
|
||||
Toast.show({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -13,6 +12,7 @@ import type { CredentialSortOrder } from '@/utils/db/repositories/SettingsReposi
|
||||
import type { Item, ItemType } from '@/utils/dist/core/models/vault';
|
||||
import { getFieldValue, FieldKey, ItemTypes } from '@/utils/dist/core/models/vault';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
import { VaultAuthenticationError } from '@/utils/types/errors/VaultAuthenticationError';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -227,9 +227,7 @@ export default function FolderViewScreen(): React.ReactNode {
|
||||
* Handle pull-to-refresh.
|
||||
*/
|
||||
const onRefresh = useCallback(async () => {
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
|
||||
setRefreshing(true);
|
||||
setIsLoadingItems(true);
|
||||
@@ -404,7 +402,7 @@ export default function FolderViewScreen(): React.ReactNode {
|
||||
const handleAddItem = useCallback(() => {
|
||||
navigate(() => {
|
||||
router.push(`/(tabs)/items/add-edit?folderId=${folderId}` as '/(tabs)/items/add-edit');
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
HapticsUtility.impact();
|
||||
});
|
||||
}, [folderId, router, navigate]);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -13,6 +12,7 @@ import type { CredentialSortOrder } from '@/utils/db/repositories/SettingsReposi
|
||||
import type { Item, ItemType } from '@/utils/dist/core/models/vault';
|
||||
import { getFieldValue, FieldKey, ItemTypes } from '@/utils/dist/core/models/vault';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
import { VaultAuthenticationError } from '@/utils/types/errors/VaultAuthenticationError';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -367,9 +367,7 @@ export default function ItemsScreen(): React.ReactNode {
|
||||
* Handle pull-to-refresh.
|
||||
*/
|
||||
const onRefresh = useCallback(async () => {
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
|
||||
setRefreshing(true);
|
||||
setIsLoadingItems(true);
|
||||
@@ -549,7 +547,7 @@ export default function ItemsScreen(): React.ReactNode {
|
||||
} else {
|
||||
router.push('/(tabs)/items/add-edit');
|
||||
}
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
HapticsUtility.impact();
|
||||
});
|
||||
}, [router, searchQuery, navigate]);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity, RefreshControl, Platform } from 'react-native';
|
||||
import { StyleSheet, View, TouchableOpacity, RefreshControl } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import type { RefreshToken } from '@/utils/dist/core/models/webapi';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -136,9 +137,7 @@ export default function ActiveSessionsScreen() : React.ReactNode {
|
||||
*/
|
||||
const onRefresh = async () : Promise<void> => {
|
||||
// Trigger haptic feedback when pull-to-refresh is activated
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
|
||||
setIsRefreshing(true);
|
||||
await loadSessions();
|
||||
@@ -175,9 +174,7 @@ export default function ActiveSessionsScreen() : React.ReactNode {
|
||||
});
|
||||
|
||||
// Trigger haptic feedback when toggling
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, RefreshControl, Platform } from 'react-native';
|
||||
import { StyleSheet, View, RefreshControl } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import type { AuthLogModel } from '@/utils/dist/core/models/webapi';
|
||||
import { AuthEventType } from '@/utils/dist/core/models/webapi';
|
||||
|
||||
@@ -109,9 +110,7 @@ export default function AuthLogsScreen() : React.ReactNode {
|
||||
*/
|
||||
const onRefresh = async () : Promise<void> => {
|
||||
// Trigger haptic feedback when pull-to-refresh is activated
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
|
||||
setIsRefreshing(true);
|
||||
await loadLogs();
|
||||
|
||||
@@ -124,9 +124,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
}
|
||||
|
||||
// Haptic feedback for successful unlock
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
router.replace('/reinitialize');
|
||||
} catch (err) {
|
||||
@@ -139,9 +137,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
const errorCode = getAppErrorCode(err);
|
||||
|
||||
// Haptic feedback for authentication error
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Error);
|
||||
|
||||
if (!errorCode || errorCode === AppErrorCode.VAULT_DECRYPT_FAILED) {
|
||||
setError(t('auth.errors.incorrectPassword'));
|
||||
@@ -204,9 +200,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
}
|
||||
|
||||
// Haptic feedback for successful unlock
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
/*
|
||||
* Navigate to reinitialize which will sync vault with server
|
||||
@@ -224,9 +218,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
const errorCode = getAppErrorCode(err);
|
||||
|
||||
// Haptic feedback for authentication error
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Error);
|
||||
|
||||
/*
|
||||
* During unlock, VAULT_DECRYPT_FAILED indicates wrong password.
|
||||
@@ -263,9 +255,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
}
|
||||
|
||||
// Haptic feedback for successful unlock
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
router.replace('/reinitialize');
|
||||
return true;
|
||||
@@ -301,9 +291,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
}
|
||||
|
||||
// Haptic feedback for successful unlock
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
router.replace('/reinitialize');
|
||||
} catch (err) {
|
||||
@@ -321,9 +309,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
await performPinUnlock();
|
||||
} else if (errorCode) {
|
||||
// Haptic feedback for authentication error
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Error);
|
||||
|
||||
// Show the error with code if no PIN fallback
|
||||
const translationKey = getErrorTranslationKey(errorCode);
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { ModalWrapper } from '@/components/common/ModalWrapper';
|
||||
|
||||
@@ -62,9 +63,7 @@ export const FolderModal: React.FC<IFolderModalProps> = ({
|
||||
await onSave(trimmedName);
|
||||
|
||||
// Haptic feedback for successful folder creation/edit
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
onClose();
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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';
|
||||
import { View, TextInput, TextInputProps, StyleSheet, Platform, TouchableOpacity, ScrollView, Switch } from 'react-native';
|
||||
|
||||
import type { PasswordSettings } from '@/utils/dist/core/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/core/password-generator';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
import { sliderToLength, lengthToSlider, SLIDER_MIN, SLIDER_MAX } from '@/utils/passwordLengthSlider';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -145,9 +145,7 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
setShowPasswordState(true);
|
||||
|
||||
// Haptic feedback for password generation
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
}
|
||||
}
|
||||
}, [currentSettings, generatePassword, onChangeText, setShowPasswordState]);
|
||||
|
||||
@@ -8,6 +8,7 @@ import DraggableFlatList, {
|
||||
|
||||
import type { FieldType } from '@/utils/dist/core/models/vault';
|
||||
import { FieldTypes } from '@/utils/dist/core/models/vault';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
@@ -162,7 +163,7 @@ export const DraggableCustomFieldsList: React.FC<DraggableCustomFieldsListProps>
|
||||
* Handle drag begin
|
||||
*/
|
||||
const handleDragBegin = useCallback(() => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
HapticsUtility.impact(Haptics.ImpactFeedbackStyle.Medium);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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';
|
||||
import { View, Text, Platform, TouchableOpacity, StyleSheet, Animated, Easing } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { copyToClipboardWithExpiration } from '@/utils/ClipboardUtility';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
@@ -107,9 +107,7 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
await copyToClipboardWithExpiration(value, timeoutSeconds);
|
||||
|
||||
// Haptic feedback for successful copy
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
|
||||
// Handle animation state
|
||||
if (timeoutSeconds > 0) {
|
||||
|
||||
47
apps/mobile-app/utils/HapticsUtility.ts
Normal file
47
apps/mobile-app/utils/HapticsUtility.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
/**
|
||||
* Utility class for managing haptic feedback across the app.
|
||||
* Provides a centralized way to trigger haptic feedback with platform checks.
|
||||
*/
|
||||
export class HapticsUtility {
|
||||
/**
|
||||
* Checks if the current platform supports haptics.
|
||||
* @returns true if platform is iOS or Android, false otherwise
|
||||
*/
|
||||
private static isHapticsAvailable(): boolean {
|
||||
return Platform.OS === 'ios' || Platform.OS === 'android';
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers impact haptic feedback (for button presses, toggles, etc.)
|
||||
* Automatically checks if the platform supports haptics (iOS/Android).
|
||||
*
|
||||
* @param style - The style of impact feedback (Light, Medium, Heavy, Rigid, Soft)
|
||||
*/
|
||||
static impact(style: Haptics.ImpactFeedbackStyle = Haptics.ImpactFeedbackStyle.Light): void {
|
||||
if (!this.isHapticsAvailable()) return;
|
||||
Haptics.impactAsync(style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers notification haptic feedback (for success, error, warning states).
|
||||
* Automatically checks if the platform supports haptics (iOS/Android).
|
||||
*
|
||||
* @param type - The type of notification feedback (Success, Warning, Error)
|
||||
*/
|
||||
static notification(type: Haptics.NotificationFeedbackType): void {
|
||||
if (!this.isHapticsAvailable()) return;
|
||||
Haptics.notificationAsync(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers selection haptic feedback (for picker scrolls, slider movements, etc.)
|
||||
* Automatically checks if the platform supports haptics (iOS/Android).
|
||||
*/
|
||||
static selection(): void {
|
||||
if (!this.isHapticsAvailable()) return;
|
||||
Haptics.selectionAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user