@umituz/react-native-haptics 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -1
- package/package.json +1 -1
- package/src/domain/entities/Haptic.ts +27 -12
- package/src/index.ts +3 -1
- package/src/infrastructure/services/HapticService.ts +32 -19
- package/src/presentation/hooks/useHaptics.ts +60 -15
package/README.md
CHANGED
|
@@ -21,8 +21,12 @@ npm install @umituz/react-native-haptics
|
|
|
21
21
|
- ✅ Selection feedback (pickers, sliders)
|
|
22
22
|
- ✅ Custom haptic patterns
|
|
23
23
|
- ✅ Convenience methods for common interactions
|
|
24
|
+
- ✅ Type guards for runtime type safety
|
|
25
|
+
- ✅ Throttle mechanism (50ms minimum interval)
|
|
26
|
+
- ✅ Development-mode error logging
|
|
24
27
|
- ✅ Silent failure (no crashes if unsupported)
|
|
25
28
|
- ✅ Platform-agnostic (iOS + Android)
|
|
29
|
+
- ✅ Performance optimized (useMemo, useCallback)
|
|
26
30
|
|
|
27
31
|
## Usage
|
|
28
32
|
|
|
@@ -91,6 +95,28 @@ await HapticService.buttonPress();
|
|
|
91
95
|
await HapticService.success();
|
|
92
96
|
```
|
|
93
97
|
|
|
98
|
+
### Type Guards
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { isImpactStyle, isNotificationType, isHapticPattern } from '@umituz/react-native-haptics';
|
|
102
|
+
|
|
103
|
+
// Validate impact style
|
|
104
|
+
if (isImpactStyle(userInput)) {
|
|
105
|
+
// TypeScript knows userInput is ImpactStyle here
|
|
106
|
+
haptics.impact(userInput);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Validate notification type
|
|
110
|
+
if (isNotificationType(type)) {
|
|
111
|
+
haptics.notification(type);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Validate haptic pattern
|
|
115
|
+
if (isHapticPattern(pattern)) {
|
|
116
|
+
haptics.pattern(pattern);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
94
120
|
## Common Patterns
|
|
95
121
|
|
|
96
122
|
- `buttonPress()` - Light impact
|
|
@@ -114,7 +140,17 @@ await HapticService.success();
|
|
|
114
140
|
|
|
115
141
|
### Utilities
|
|
116
142
|
|
|
117
|
-
- `
|
|
143
|
+
- `HAPTIC_CONSTANTS`: Constants for default haptic styles
|
|
144
|
+
- `isImpactStyle()`: Type guard for ImpactStyle validation
|
|
145
|
+
- `isNotificationType()`: Type guard for NotificationType validation
|
|
146
|
+
- `isHapticPattern()`: Type guard for HapticPattern validation
|
|
147
|
+
|
|
148
|
+
## Performance
|
|
149
|
+
|
|
150
|
+
- **Throttle**: 50ms minimum interval between haptic feedback to prevent spam
|
|
151
|
+
- **Memoization**: Hook return value is memoized with useMemo
|
|
152
|
+
- **Callback optimization**: All callbacks use useCallback to prevent re-renders
|
|
153
|
+
- **Development logging**: Errors are logged only in development mode (__DEV__)
|
|
118
154
|
|
|
119
155
|
## License
|
|
120
156
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-haptics",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Haptic feedback (vibration) for React Native using expo-haptics with impact, notification, and selection feedback patterns",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -22,9 +22,9 @@ export type NotificationType = 'Success' | 'Warning' | 'Error';
|
|
|
22
22
|
* Haptic patterns for common interactions
|
|
23
23
|
*/
|
|
24
24
|
export type HapticPattern =
|
|
25
|
-
| '
|
|
26
|
-
| '
|
|
27
|
-
| '
|
|
25
|
+
| 'success'
|
|
26
|
+
| 'warning'
|
|
27
|
+
| 'error'
|
|
28
28
|
| 'selection';
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -38,14 +38,29 @@ export const HAPTIC_CONSTANTS = {
|
|
|
38
38
|
} as const;
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
* Simplified - pattern handling moved to HapticService
|
|
41
|
+
* Type guards for runtime type safety
|
|
43
42
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if value is a valid ImpactStyle
|
|
46
|
+
*/
|
|
47
|
+
export function isImpactStyle(value: unknown): value is ImpactStyle {
|
|
48
|
+
return typeof value === 'string' &&
|
|
49
|
+
['Light', 'Medium', 'Heavy'].includes(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if value is a valid NotificationType
|
|
54
|
+
*/
|
|
55
|
+
export function isNotificationType(value: unknown): value is NotificationType {
|
|
56
|
+
return typeof value === 'string' &&
|
|
57
|
+
['Success', 'Warning', 'Error'].includes(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if value is a valid HapticPattern
|
|
62
|
+
*/
|
|
63
|
+
export function isHapticPattern(value: unknown): value is HapticPattern {
|
|
64
|
+
return typeof value === 'string' &&
|
|
65
|
+
['success', 'warning', 'error', 'selection'].includes(value);
|
|
51
66
|
}
|
package/src/index.ts
CHANGED
|
@@ -163,7 +163,9 @@ export type {
|
|
|
163
163
|
|
|
164
164
|
export {
|
|
165
165
|
HAPTIC_CONSTANTS,
|
|
166
|
-
|
|
166
|
+
isImpactStyle,
|
|
167
|
+
isNotificationType,
|
|
168
|
+
isHapticPattern,
|
|
167
169
|
} from './domain/entities/Haptic';
|
|
168
170
|
|
|
169
171
|
// ============================================================================
|
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
import * as Haptics from 'expo-haptics';
|
|
12
12
|
import type { ImpactStyle, NotificationType, HapticPattern } from '../../domain/entities/Haptic';
|
|
13
|
+
import { isImpactStyle, isNotificationType, isHapticPattern } from '../../domain/entities/Haptic';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Log error in development mode only
|
|
17
|
+
*/
|
|
18
|
+
function logError(method: string, error: unknown): void {
|
|
19
|
+
if (process.env.NODE_ENV === 'development') {
|
|
20
|
+
console.error(`[HapticService.${method}]`, error);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
13
23
|
|
|
14
24
|
|
|
15
25
|
/**
|
|
@@ -27,7 +37,7 @@ export class HapticService {
|
|
|
27
37
|
Haptics.ImpactFeedbackStyle.Heavy
|
|
28
38
|
);
|
|
29
39
|
} catch (error) {
|
|
30
|
-
|
|
40
|
+
logError('impact', error);
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
|
|
@@ -42,7 +52,7 @@ export class HapticService {
|
|
|
42
52
|
Haptics.NotificationFeedbackType.Error
|
|
43
53
|
);
|
|
44
54
|
} catch (error) {
|
|
45
|
-
|
|
55
|
+
logError('notification', error);
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
|
|
@@ -53,7 +63,7 @@ export class HapticService {
|
|
|
53
63
|
try {
|
|
54
64
|
await Haptics.selectionAsync();
|
|
55
65
|
} catch (error) {
|
|
56
|
-
|
|
66
|
+
logError('selection', error);
|
|
57
67
|
}
|
|
58
68
|
}
|
|
59
69
|
|
|
@@ -62,21 +72,24 @@ export class HapticService {
|
|
|
62
72
|
*/
|
|
63
73
|
static async pattern(pattern: HapticPattern): Promise<void> {
|
|
64
74
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
switch (pattern) {
|
|
76
|
+
case 'selection':
|
|
77
|
+
await HapticService.selection();
|
|
78
|
+
break;
|
|
79
|
+
case 'success':
|
|
80
|
+
await HapticService.notification('Success');
|
|
81
|
+
break;
|
|
82
|
+
case 'warning':
|
|
83
|
+
await HapticService.notification('Warning');
|
|
84
|
+
break;
|
|
85
|
+
case 'error':
|
|
86
|
+
await HapticService.notification('Error');
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
await HapticService.impact('Light');
|
|
74
90
|
}
|
|
75
|
-
|
|
76
|
-
// Default to light impact
|
|
77
|
-
await HapticService.impact('Light');
|
|
78
91
|
} catch (error) {
|
|
79
|
-
|
|
92
|
+
logError('pattern', error);
|
|
80
93
|
}
|
|
81
94
|
}
|
|
82
95
|
|
|
@@ -88,15 +101,15 @@ export class HapticService {
|
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
static async success(): Promise<void> {
|
|
91
|
-
await HapticService.pattern('
|
|
104
|
+
await HapticService.pattern('success');
|
|
92
105
|
}
|
|
93
106
|
|
|
94
107
|
static async error(): Promise<void> {
|
|
95
|
-
await HapticService.pattern('
|
|
108
|
+
await HapticService.pattern('error');
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
static async warning(): Promise<void> {
|
|
99
|
-
await HapticService.pattern('
|
|
112
|
+
await HapticService.pattern('warning');
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
static async delete(): Promise<void> {
|
|
@@ -8,10 +8,16 @@
|
|
|
8
8
|
* @layer presentation/hooks
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useCallback } from 'react';
|
|
11
|
+
import { useCallback, useRef, useMemo } from 'react';
|
|
12
12
|
import { HapticService } from '../../infrastructure/services/HapticService';
|
|
13
13
|
import type { ImpactStyle, NotificationType, HapticPattern } from '../../domain/entities/Haptic';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Minimum interval between haptic feedback (ms)
|
|
17
|
+
* Prevents spam and improves UX
|
|
18
|
+
*/
|
|
19
|
+
const THROTTLE_INTERVAL = 50;
|
|
20
|
+
|
|
15
21
|
/**
|
|
16
22
|
* useHaptics hook for haptic feedback
|
|
17
23
|
*
|
|
@@ -45,70 +51,96 @@ import type { ImpactStyle, NotificationType, HapticPattern } from '../../domain/
|
|
|
45
51
|
* ```
|
|
46
52
|
*/
|
|
47
53
|
export const useHaptics = () => {
|
|
54
|
+
const lastExecutionRef = useRef<number>(0);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if enough time has passed since last haptic
|
|
58
|
+
*/
|
|
59
|
+
const canExecute = useCallback((): boolean => {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
if (now - lastExecutionRef.current < THROTTLE_INTERVAL) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
lastExecutionRef.current = now;
|
|
65
|
+
return true;
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
48
68
|
/**
|
|
49
69
|
* Trigger impact feedback (light, medium, heavy)
|
|
50
70
|
*/
|
|
51
71
|
const impact = useCallback(async (style: ImpactStyle = 'Light') => {
|
|
72
|
+
if (!canExecute()) return;
|
|
52
73
|
await HapticService.impact(style);
|
|
53
|
-
}, []);
|
|
74
|
+
}, [canExecute]);
|
|
54
75
|
|
|
55
76
|
/**
|
|
56
77
|
* Trigger notification feedback (success, warning, error)
|
|
57
78
|
*/
|
|
58
79
|
const notification = useCallback(async (type: NotificationType) => {
|
|
80
|
+
if (!canExecute()) return;
|
|
59
81
|
await HapticService.notification(type);
|
|
60
|
-
}, []);
|
|
82
|
+
}, [canExecute]);
|
|
61
83
|
|
|
62
84
|
/**
|
|
63
85
|
* Trigger selection feedback (for pickers, sliders)
|
|
64
86
|
*/
|
|
65
87
|
const selection = useCallback(async () => {
|
|
88
|
+
if (!canExecute()) return;
|
|
66
89
|
await HapticService.selection();
|
|
67
|
-
}, []);
|
|
90
|
+
}, [canExecute]);
|
|
68
91
|
|
|
69
92
|
/**
|
|
70
93
|
* Trigger custom haptic pattern
|
|
71
94
|
*/
|
|
72
95
|
const pattern = useCallback(async (patternType: HapticPattern) => {
|
|
96
|
+
if (!canExecute()) return;
|
|
73
97
|
await HapticService.pattern(patternType);
|
|
74
|
-
}, []);
|
|
98
|
+
}, [canExecute]);
|
|
75
99
|
|
|
76
100
|
/**
|
|
77
101
|
* Common haptic patterns (convenience methods)
|
|
78
102
|
*/
|
|
79
103
|
const buttonPress = useCallback(async () => {
|
|
104
|
+
if (!canExecute()) return;
|
|
80
105
|
await HapticService.buttonPress();
|
|
81
|
-
}, []);
|
|
106
|
+
}, [canExecute]);
|
|
82
107
|
|
|
83
108
|
const success = useCallback(async () => {
|
|
109
|
+
if (!canExecute()) return;
|
|
84
110
|
await HapticService.success();
|
|
85
|
-
}, []);
|
|
111
|
+
}, [canExecute]);
|
|
86
112
|
|
|
87
113
|
const error = useCallback(async () => {
|
|
114
|
+
if (!canExecute()) return;
|
|
88
115
|
await HapticService.error();
|
|
89
|
-
}, []);
|
|
116
|
+
}, [canExecute]);
|
|
90
117
|
|
|
91
118
|
const warning = useCallback(async () => {
|
|
119
|
+
if (!canExecute()) return;
|
|
92
120
|
await HapticService.warning();
|
|
93
|
-
}, []);
|
|
121
|
+
}, [canExecute]);
|
|
94
122
|
|
|
95
123
|
const deleteItem = useCallback(async () => {
|
|
124
|
+
if (!canExecute()) return;
|
|
96
125
|
await HapticService.delete();
|
|
97
|
-
}, []);
|
|
126
|
+
}, [canExecute]);
|
|
98
127
|
|
|
99
128
|
const refresh = useCallback(async () => {
|
|
129
|
+
if (!canExecute()) return;
|
|
100
130
|
await HapticService.refresh();
|
|
101
|
-
}, []);
|
|
131
|
+
}, [canExecute]);
|
|
102
132
|
|
|
103
133
|
const selectionChange = useCallback(async () => {
|
|
134
|
+
if (!canExecute()) return;
|
|
104
135
|
await HapticService.selectionChange();
|
|
105
|
-
}, []);
|
|
136
|
+
}, [canExecute]);
|
|
106
137
|
|
|
107
138
|
const longPress = useCallback(async () => {
|
|
139
|
+
if (!canExecute()) return;
|
|
108
140
|
await HapticService.longPress();
|
|
109
|
-
}, []);
|
|
141
|
+
}, [canExecute]);
|
|
110
142
|
|
|
111
|
-
return {
|
|
143
|
+
return useMemo(() => ({
|
|
112
144
|
// Generic methods
|
|
113
145
|
impact,
|
|
114
146
|
notification,
|
|
@@ -124,5 +156,18 @@ export const useHaptics = () => {
|
|
|
124
156
|
refresh,
|
|
125
157
|
selectionChange,
|
|
126
158
|
longPress,
|
|
127
|
-
}
|
|
159
|
+
}), [
|
|
160
|
+
impact,
|
|
161
|
+
notification,
|
|
162
|
+
selection,
|
|
163
|
+
pattern,
|
|
164
|
+
buttonPress,
|
|
165
|
+
success,
|
|
166
|
+
error,
|
|
167
|
+
warning,
|
|
168
|
+
deleteItem,
|
|
169
|
+
refresh,
|
|
170
|
+
selectionChange,
|
|
171
|
+
longPress,
|
|
172
|
+
]);
|
|
128
173
|
};
|