Skip to content

Commit d2779d8

Browse files
committed
feat: added native toggle to turn haptics on/off
1 parent ea9d64a commit d2779d8

File tree

6 files changed

+148
-3
lines changed

6 files changed

+148
-3
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,33 @@ export function SomeScreen() {
192192
}
193193
```
194194

195-
## API (tables)
195+
### Global enable/disable (settings toggle)
196+
197+
Disable haptics globally for users who prefer no haptic feedback. The setting is **persisted** across app restarts. When disabled, all haptic calls become no-ops.
198+
199+
**Using the hook (recommended):**
200+
201+
```tsx
202+
import { useHapticsEnabled } from 'react-native-ahaps';
203+
204+
function SettingsScreen() {
205+
const [hapticsEnabled, setHapticsEnabled] = useHapticsEnabled();
206+
207+
return <Switch value={hapticsEnabled} onValueChange={setHapticsEnabled} />;
208+
}
209+
```
210+
211+
**Manual control:**
212+
213+
```ts
214+
import { setHapticsEnabled, getHapticsEnabled } from 'react-native-ahaps';
215+
216+
const isEnabled = getHapticsEnabled(); // true by default
217+
setHapticsEnabled(false); // Disable all haptics
218+
setHapticsEnabled(true); // Re-enable haptics
219+
```
220+
221+
## API
196222

197223
| Function | Purpose |
198224
| ---------------------------------------------------------------------- | --------------------------------------------------------------- |
@@ -201,6 +227,8 @@ export function SomeScreen() {
201227
| `initializeEngine()` / `destroyEngine()` | Manual engine lifecycle |
202228
| `startHaptic(events, curves)` | Play a pattern (transient + continuous events, optional curves) |
203229
| `stopAllHaptics()` | Stop any running haptics (useful on unmount/navigation) |
230+
| `useHapticsEnabled()` | Hook for reactive haptics enabled state |
231+
| `setHapticsEnabled(enabled)` / `getHapticsEnabled()` | Manual enable/disable toggle (persisted) |
204232
| `useContinuousPlayer(playerId, initialIntensity, initialSharpness)` | Hook to manage a continuous player lifecycle |
205233
| `createContinuousPlayer(playerId, initialIntensity, initialSharpness)` | Create a continuous player with given ID |
206234
| `startContinuousPlayer(playerId)` / `stopContinuousPlayer(playerId)` | Start/stop continuous playback for player |

example/src/screens/Menu.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
1+
import { View, Text, StyleSheet, TouchableOpacity, Switch } from 'react-native';
22
import { useRouter } from 'expo-router';
33
import { useSafeAreaInsets } from 'react-native-safe-area-context';
44
import { useTheme } from '../contexts/ThemeContext';
5+
import { useHapticsEnabled } from 'react-native-ahaps';
56

67
export function Menu() {
78
const router = useRouter();
89
const insets = useSafeAreaInsets();
910
const { colors } = useTheme();
11+
const [hapticsEnabled, setHapticsEnabled] = useHapticsEnabled();
1012

1113
return (
1214
<View
@@ -63,6 +65,17 @@ export function Menu() {
6365
Record haptics in real-time using touch gestures
6466
</Text>
6567
</TouchableOpacity>
68+
69+
<View style={[styles.toggleItem, { backgroundColor: colors.card }]}>
70+
<Text style={[styles.toggleText, { color: colors.text }]}>
71+
Haptics {hapticsEnabled ? 'Enabled' : 'Disabled'}
72+
</Text>
73+
<Switch
74+
value={hapticsEnabled}
75+
onValueChange={setHapticsEnabled}
76+
trackColor={{ false: colors.secondaryText, true: colors.accent }}
77+
/>
78+
</View>
6679
</View>
6780
</View>
6881
);
@@ -92,4 +105,15 @@ const styles = StyleSheet.create({
92105
menuDescription: {
93106
fontSize: 16,
94107
},
108+
toggleItem: {
109+
flexDirection: 'row',
110+
alignItems: 'center',
111+
justifyContent: 'space-between',
112+
padding: 24,
113+
borderRadius: 16,
114+
},
115+
toggleText: {
116+
fontSize: 18,
117+
fontWeight: '600',
118+
},
95119
});

ios/Ahap.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ class Ahap: HybridAhapSpec {
4545
func destroyContinuousPlayer(playerId: String) throws {
4646
haptics.destroyContinuousPlayer(playerId: playerId)
4747
}
48+
49+
// MARK: - Global Haptics Enable/Disable
50+
51+
func setHapticsEnabled(enabled: Bool) throws {
52+
haptics.hapticsEnabled = enabled
53+
}
54+
55+
func getHapticsEnabled() throws -> Bool {
56+
return haptics.hapticsEnabled
57+
}
4858

4959

5060
func startHaptic(events: [HapticEvent], curves: [HapticCurve]) throws {

ios/AhapUtils.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ struct ContinuousPlayerConfig {
3030

3131
class HapticFeedback {
3232
static let shared = HapticFeedback()
33+
34+
private static let hapticsEnabledKey = "com.ahap.hapticsEnabled"
3335

3436
private var engine: CHHapticEngine?
3537
private var hapticPlayers: [String: CHHapticAdvancedPatternPlayer] = [:]
@@ -41,6 +43,23 @@ class HapticFeedback {
4143
public var supportsHaptics: Bool {
4244
return CHHapticEngine.capabilitiesForHardware().supportsHaptics
4345
}
46+
47+
// MARK: - Global Haptics Enable/Disable
48+
49+
/// Returns whether haptics are globally enabled. Persisted in UserDefaults.
50+
/// Defaults to true if not previously set.
51+
public var hapticsEnabled: Bool {
52+
get {
53+
// If key doesn't exist, default to true (haptics enabled)
54+
if UserDefaults.standard.object(forKey: HapticFeedback.hapticsEnabledKey) == nil {
55+
return true
56+
}
57+
return UserDefaults.standard.bool(forKey: HapticFeedback.hapticsEnabledKey)
58+
}
59+
set {
60+
UserDefaults.standard.set(newValue, forKey: HapticFeedback.hapticsEnabledKey)
61+
}
62+
}
4463

4564
public func createAndStartHapticEngine() {
4665
guard supportsHaptics else {
@@ -175,6 +194,8 @@ class HapticFeedback {
175194
}
176195

177196
public func startHapticPlayer<State: HapticAnimationState>(for state: State) {
197+
guard hapticsEnabled else { return }
198+
178199
lock.lock()
179200
defer { lock.unlock() }
180201

@@ -283,7 +304,10 @@ class HapticFeedback {
283304

284305
/// Starts the continuous haptic player with the given ID.
285306
/// - If the player doesn't exist (not created or already destroyed), this is a safe no-op.
307+
/// - If haptics are globally disabled, this is a no-op.
286308
public func startContinuousPlayer(playerId: String) {
309+
guard hapticsEnabled else { return }
310+
287311
lock.lock()
288312
defer { lock.unlock() }
289313

src/Ahap.nitro.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,7 @@ export interface Ahap
4545
): void;
4646
stopContinuousPlayer(playerId: string): void;
4747
destroyContinuousPlayer(playerId: string): void;
48+
49+
setHapticsEnabled(enabled: boolean): void;
50+
getHapticsEnabled(): boolean;
4851
}

src/index.tsx

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect } from 'react';
1+
import { useEffect, useState, useCallback } from 'react';
22
import { AppState } from 'react-native';
33
import { NitroModules } from 'react-native-nitro-modules';
44
import type {
@@ -94,6 +94,62 @@ export function destroyContinuousPlayer(playerId: string): void {
9494
return boxedAhap.unbox().destroyContinuousPlayer(playerId);
9595
}
9696

97+
// MARK: - Global Haptics Enable/Disable
98+
99+
/**
100+
* Enable or disable haptics globally. This setting is persisted across app restarts.
101+
* When disabled, all haptic functions become no-ops (no haptics will play).
102+
* This does not affect engine initialization/destruction.
103+
*
104+
* @param enabled - Whether haptics should be enabled
105+
*/
106+
export function setHapticsEnabled(enabled: boolean): void {
107+
'worklet';
108+
return boxedAhap.unbox().setHapticsEnabled(enabled);
109+
}
110+
111+
/**
112+
* Get the current global haptics enabled state.
113+
* Defaults to true if not previously set.
114+
*
115+
* @returns Whether haptics are currently enabled
116+
*/
117+
export function getHapticsEnabled(): boolean {
118+
'worklet';
119+
return boxedAhap.unbox().getHapticsEnabled();
120+
}
121+
122+
/**
123+
* Hook to manage the global haptics enabled state.
124+
* Provides reactive state and a setter function.
125+
*
126+
* @returns [isEnabled, setEnabled] - Current state and setter function
127+
*
128+
* @example
129+
* ```tsx
130+
* function SettingsScreen() {
131+
* const [hapticsEnabled, setHapticsEnabled] = useHapticsEnabled();
132+
*
133+
* return (
134+
* <Switch
135+
* value={hapticsEnabled}
136+
* onValueChange={setHapticsEnabled}
137+
* />
138+
* );
139+
* }
140+
* ```
141+
*/
142+
export function useHapticsEnabled(): [boolean, (enabled: boolean) => void] {
143+
const [enabled, setEnabled] = useState(() => getHapticsEnabled());
144+
145+
const setHapticsEnabledState = useCallback((value: boolean) => {
146+
setHapticsEnabled(value);
147+
setEnabled(value);
148+
}, []);
149+
150+
return [enabled, setHapticsEnabledState] as const;
151+
}
152+
97153
/**
98154
* Hook to manage a continuous haptic player lifecycle.
99155
*

0 commit comments

Comments
 (0)