yaver-feedback-react-native 0.5.0 → 0.5.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaver-feedback-react-native",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Visual feedback SDK for Yaver — shake-to-report, screen recording, voice annotations for vibe coding",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -1,57 +1,117 @@
1
1
  import { DeviceEventEmitter, Platform } from 'react-native';
2
2
 
3
- const SHAKE_THRESHOLD = 1.5; // g-force threshold
4
3
  const SHAKE_TIMEOUT_MS = 1000; // minimum time between shakes
5
- const SHAKE_REQUIRED_EVENTS = 3; // number of threshold-exceeding events to trigger
4
+ const ACCEL_THRESHOLD_G = 1.8; // peak g-force that qualifies as a shake event
5
+ const ACCEL_MIN_HITS = 3; // peaks within the window before we fire
6
+ const ACCEL_WINDOW_MS = 800; // rolling window
7
+ const ACCEL_SAMPLE_INTERVAL_MS = 80; // ≈12Hz — cheap but catches shakes
6
8
 
7
9
  /**
8
10
  * Detects device shake gestures.
9
11
  *
10
- * On iOS, listens to the native 'shakeEvent' emitted by RCTDeviceEventEmitter.
11
- * On Android, uses accelerometer data with threshold-based detection as a fallback.
12
+ * Two detection paths run in parallel so shake works in both Debug and
13
+ * Release / TestFlight builds:
14
+ *
15
+ * 1. React Native's built-in `shakeEvent` (iOS) / `ShakeEvent` (Android)
16
+ * on `DeviceEventEmitter`. RN emits `shakeEvent` only in Debug mode
17
+ * on iOS, which is why TestFlight builds never saw a shake prior to
18
+ * SDK 0.5.1.
19
+ * 2. Accelerometer-based fallback via `expo-sensors` (optional peer
20
+ * dep). When the host app has `expo-sensors` installed we subscribe
21
+ * to the accelerometer and fire on a burst of above-threshold peaks.
22
+ * This path works identically in Debug, Release, and TestFlight.
23
+ *
24
+ * Both paths share a 1-second debounce so a single shake never fires the
25
+ * callback twice.
12
26
  */
13
27
  export class ShakeDetector {
14
- private subscription: any = null;
28
+ private devMenuSub: { remove(): void } | null = null;
29
+ private accelSub: { remove(): void } | null = null;
15
30
  private lastShakeTime = 0;
31
+ private peakTimestamps: number[] = [];
16
32
 
17
- /**
18
- * Start listening for shake gestures.
19
- * @param onShake - Callback invoked when a shake is detected.
20
- */
21
33
  start(onShake: () => void): void {
22
34
  this.stop();
35
+ this.subscribeDevMenu(onShake);
36
+ this.subscribeAccelerometer(onShake);
37
+ }
23
38
 
24
- // React Native emits a 'shakeEvent' on iOS when the device is shaken
25
- // (available via DeviceEventEmitter in debug builds)
26
- if (Platform.OS === 'ios') {
27
- this.subscription = DeviceEventEmitter.addListener('shakeEvent', () => {
28
- const now = Date.now();
29
- if (now - this.lastShakeTime > SHAKE_TIMEOUT_MS) {
30
- this.lastShakeTime = now;
31
- onShake();
32
- }
33
- });
34
- return;
39
+ stop(): void {
40
+ if (this.devMenuSub) {
41
+ this.devMenuSub.remove();
42
+ this.devMenuSub = null;
35
43
  }
44
+ if (this.accelSub) {
45
+ this.accelSub.remove();
46
+ this.accelSub = null;
47
+ }
48
+ this.peakTimestamps = [];
49
+ }
36
50
 
37
- // Android: listen for accelerometer-based shake detection
38
- // Uses the same DeviceEventEmitter pattern — if a native module or
39
- // react-native-shake is installed, it will emit 'ShakeEvent'.
40
- // Otherwise this is a no-op (manual trigger or floating button can be used).
41
- this.subscription = DeviceEventEmitter.addListener('ShakeEvent', () => {
42
- const now = Date.now();
43
- if (now - this.lastShakeTime > SHAKE_TIMEOUT_MS) {
44
- this.lastShakeTime = now;
45
- onShake();
46
- }
51
+ private fire(onShake: () => void): void {
52
+ const now = Date.now();
53
+ if (now - this.lastShakeTime <= SHAKE_TIMEOUT_MS) return;
54
+ this.lastShakeTime = now;
55
+ this.peakTimestamps = [];
56
+ onShake();
57
+ }
58
+
59
+ /**
60
+ * Dev-menu / platform-native event listener. On iOS this is the
61
+ * `shakeEvent` RN posts from RCTDevMenu (Debug only). On Android it is
62
+ * the `ShakeEvent` name a handful of third-party shake libraries emit.
63
+ */
64
+ private subscribeDevMenu(onShake: () => void): void {
65
+ const eventName = Platform.OS === 'ios' ? 'shakeEvent' : 'ShakeEvent';
66
+ this.devMenuSub = DeviceEventEmitter.addListener(eventName, () => {
67
+ this.fire(onShake);
47
68
  });
48
69
  }
49
70
 
50
- /** Stop listening for shake gestures. */
51
- stop(): void {
52
- if (this.subscription) {
53
- this.subscription.remove();
54
- this.subscription = null;
71
+ /**
72
+ * Accelerometer-based detection. Uses `expo-sensors` when available —
73
+ * the host app's own dependency, not the SDK's, so apps that don't
74
+ * want the extra native surface get the Dev-menu path only.
75
+ */
76
+ private subscribeAccelerometer(onShake: () => void): void {
77
+ let Accelerometer: {
78
+ setUpdateInterval: (ms: number) => void;
79
+ addListener: (cb: (d: { x: number; y: number; z: number }) => void) => {
80
+ remove(): void;
81
+ };
82
+ } | null = null;
83
+ try {
84
+ // Optional peer dep — if it isn't installed, we simply skip this path.
85
+ Accelerometer = require('expo-sensors').Accelerometer;
86
+ } catch {
87
+ return;
88
+ }
89
+ if (!Accelerometer) return;
90
+
91
+ try {
92
+ Accelerometer.setUpdateInterval(ACCEL_SAMPLE_INTERVAL_MS);
93
+ } catch {
94
+ // Some platforms reject zero-value intervals; fall through with defaults
55
95
  }
96
+
97
+ this.accelSub = Accelerometer.addListener(({ x, y, z }) => {
98
+ // Magnitude of acceleration vector (in g). Subtract 1 so a stationary
99
+ // device reports ~0 rather than the 1g of gravity.
100
+ const mag = Math.sqrt(x * x + y * y + z * z);
101
+ if (mag - 1 < ACCEL_THRESHOLD_G - 1) return;
102
+
103
+ const now = Date.now();
104
+ this.peakTimestamps.push(now);
105
+ // Drop peaks that fell out of the rolling window.
106
+ while (
107
+ this.peakTimestamps.length > 0 &&
108
+ now - this.peakTimestamps[0] > ACCEL_WINDOW_MS
109
+ ) {
110
+ this.peakTimestamps.shift();
111
+ }
112
+ if (this.peakTimestamps.length >= ACCEL_MIN_HITS) {
113
+ this.fire(onShake);
114
+ }
115
+ });
56
116
  }
57
117
  }