yaver-feedback-react-native 0.5.3 → 0.5.5

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.
Files changed (52) hide show
  1. package/README.md +30 -0
  2. package/dist/AuthOverlay.d.ts +16 -0
  3. package/dist/AuthOverlay.js +104 -0
  4. package/dist/BlackBox.d.ts +154 -0
  5. package/dist/BlackBox.js +395 -0
  6. package/dist/ConnectionScreen.d.ts +13 -0
  7. package/dist/ConnectionScreen.js +373 -0
  8. package/dist/Discovery.d.ts +59 -0
  9. package/dist/Discovery.js +293 -0
  10. package/dist/FeedbackModal.d.ts +11 -0
  11. package/dist/FeedbackModal.js +623 -0
  12. package/dist/FixReport.d.ts +23 -0
  13. package/dist/FixReport.js +282 -0
  14. package/dist/FloatingButton.d.ts +71 -0
  15. package/dist/FloatingButton.js +778 -0
  16. package/dist/LoginScreen.d.ts +14 -0
  17. package/dist/LoginScreen.js +317 -0
  18. package/dist/MachinePickerScreen.d.ts +19 -0
  19. package/dist/MachinePickerScreen.js +175 -0
  20. package/dist/P2PClient.d.ts +136 -0
  21. package/dist/P2PClient.js +357 -0
  22. package/dist/ShakeDetector.d.ts +39 -0
  23. package/dist/ShakeDetector.js +133 -0
  24. package/dist/YaverFeedback.d.ts +198 -0
  25. package/dist/YaverFeedback.js +707 -0
  26. package/dist/YaverUpdates.d.ts +78 -0
  27. package/dist/YaverUpdates.js +272 -0
  28. package/dist/__tests__/Discovery.test.d.ts +1 -0
  29. package/dist/__tests__/Discovery.test.js +164 -0
  30. package/dist/__tests__/P2PClient.test.d.ts +1 -0
  31. package/dist/__tests__/P2PClient.test.js +169 -0
  32. package/dist/__tests__/SDKToken.test.d.ts +1 -0
  33. package/dist/__tests__/SDKToken.test.js +215 -0
  34. package/dist/__tests__/YaverFeedback.test.d.ts +1 -0
  35. package/dist/__tests__/YaverFeedback.test.js +161 -0
  36. package/dist/__tests__/types.test.d.ts +1 -0
  37. package/dist/__tests__/types.test.js +219 -0
  38. package/dist/auth.d.ts +105 -0
  39. package/dist/auth.js +282 -0
  40. package/dist/capture.d.ts +27 -0
  41. package/dist/capture.js +74 -0
  42. package/dist/expo.d.ts +15 -0
  43. package/dist/expo.js +62 -0
  44. package/dist/index.d.ts +48 -0
  45. package/dist/index.js +80 -0
  46. package/dist/types.d.ts +282 -0
  47. package/dist/types.js +2 -0
  48. package/dist/upload.d.ts +13 -0
  49. package/dist/upload.js +59 -0
  50. package/package.json +6 -3
  51. package/src/ShakeDetector.ts +22 -1
  52. package/src/YaverFeedback.ts +29 -0
@@ -0,0 +1,357 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.P2PClient = void 0;
4
+ const react_native_1 = require("react-native");
5
+ /**
6
+ * Lightweight P2P HTTP client for communicating with a Yaver agent.
7
+ *
8
+ * Reuses the same endpoint patterns as the main upload module but adds
9
+ * support for streaming feedback, listing builds, and triggering builds.
10
+ */
11
+ class P2PClient {
12
+ constructor(baseUrl, authToken) {
13
+ this.baseUrl = baseUrl.replace(/\/$/, '');
14
+ this.authToken = authToken;
15
+ }
16
+ /** Update the base URL (e.g. after re-discovery). */
17
+ setBaseUrl(url) {
18
+ this.baseUrl = url.replace(/\/$/, '');
19
+ }
20
+ /** Update the auth token. */
21
+ setAuthToken(token) {
22
+ this.authToken = token;
23
+ }
24
+ /** Health check — returns true if the agent is reachable. */
25
+ async health() {
26
+ try {
27
+ const controller = new AbortController();
28
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
29
+ const response = await fetch(`${this.baseUrl}/health`, {
30
+ method: 'GET',
31
+ signal: controller.signal,
32
+ });
33
+ clearTimeout(timeoutId);
34
+ return response.ok;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ /** Get agent info (hostname, version, platform). */
41
+ async info() {
42
+ const response = await this.request('GET', '/health');
43
+ const data = await response.json();
44
+ return {
45
+ hostname: data.hostname ?? data.name ?? 'Unknown',
46
+ version: data.version ?? 'unknown',
47
+ platform: data.platform ?? 'unknown',
48
+ };
49
+ }
50
+ /**
51
+ * Upload a feedback bundle via multipart POST.
52
+ * @returns The feedback report ID from the agent.
53
+ */
54
+ async uploadFeedback(bundle) {
55
+ const formData = new FormData();
56
+ formData.append('metadata', JSON.stringify(bundle.metadata));
57
+ for (let i = 0; i < bundle.screenshots.length; i++) {
58
+ const path = bundle.screenshots[i];
59
+ formData.append(`screenshot_${i}`, {
60
+ uri: react_native_1.Platform.OS === 'android' ? `file://${path}` : path,
61
+ type: 'image/png',
62
+ name: `screenshot_${i}.png`,
63
+ });
64
+ }
65
+ if (bundle.audio) {
66
+ formData.append('audio', {
67
+ uri: react_native_1.Platform.OS === 'android' ? `file://${bundle.audio}` : bundle.audio,
68
+ type: 'audio/m4a',
69
+ name: 'voice_note.m4a',
70
+ });
71
+ }
72
+ if (bundle.video) {
73
+ formData.append('video', {
74
+ uri: react_native_1.Platform.OS === 'android' ? `file://${bundle.video}` : bundle.video,
75
+ type: 'video/mp4',
76
+ name: 'screen_recording.mp4',
77
+ });
78
+ }
79
+ const response = await fetch(`${this.baseUrl}/feedback`, {
80
+ method: 'POST',
81
+ headers: {
82
+ Authorization: `Bearer ${this.authToken}`,
83
+ },
84
+ body: formData,
85
+ });
86
+ if (!response.ok) {
87
+ const text = await response.text().catch(() => '');
88
+ throw new Error(`[P2PClient] Upload failed (${response.status}): ${text}`);
89
+ }
90
+ const result = await response.json();
91
+ return result.id ?? result.reportId ?? 'unknown';
92
+ }
93
+ /**
94
+ * Stream feedback events to the agent in live mode.
95
+ * Sends each event as a JSON POST to `/feedback/stream`.
96
+ */
97
+ async streamFeedback(events) {
98
+ for await (const event of events) {
99
+ const response = await fetch(`${this.baseUrl}/feedback/stream`, {
100
+ method: 'POST',
101
+ headers: {
102
+ Authorization: `Bearer ${this.authToken}`,
103
+ 'Content-Type': 'application/json',
104
+ },
105
+ body: JSON.stringify(event),
106
+ });
107
+ if (!response.ok) {
108
+ const text = await response.text().catch(() => '');
109
+ throw new Error(`[P2PClient] Stream event failed (${response.status}): ${text}`);
110
+ }
111
+ }
112
+ }
113
+ /** List available builds from the agent. */
114
+ async listBuilds() {
115
+ const response = await this.request('GET', '/builds');
116
+ const data = await response.json();
117
+ return data.builds ?? data ?? [];
118
+ }
119
+ /** Start a build for the given platform. */
120
+ async startBuild(platform) {
121
+ const response = await fetch(`${this.baseUrl}/builds`, {
122
+ method: 'POST',
123
+ headers: {
124
+ Authorization: `Bearer ${this.authToken}`,
125
+ 'Content-Type': 'application/json',
126
+ },
127
+ body: JSON.stringify({ platform }),
128
+ });
129
+ if (!response.ok) {
130
+ const text = await response.text().catch(() => '');
131
+ throw new Error(`[P2PClient] Start build failed (${response.status}): ${text}`);
132
+ }
133
+ return response.json();
134
+ }
135
+ /**
136
+ * Get voice capability info from the agent.
137
+ * voiceInputEnabled is always true — mobile can always record and send audio.
138
+ * s2sProvider/sttProvider indicate whether transcription is available.
139
+ */
140
+ async voiceStatus() {
141
+ const response = await this.request('GET', '/voice/status');
142
+ const data = await response.json();
143
+ return {
144
+ voiceInputEnabled: data.voiceInputEnabled ?? true,
145
+ s2sProvider: data.s2sProvider ?? undefined,
146
+ s2sReady: data.s2sReady ?? false,
147
+ sttProvider: data.sttProvider ?? undefined,
148
+ sttReady: data.sttReady ?? false,
149
+ };
150
+ }
151
+ /**
152
+ * Send voice audio to the agent for transcription.
153
+ * Works with any configured STT or S2S provider on the agent.
154
+ * If no provider is configured, audio is saved for manual review.
155
+ * @returns Transcribed text (or empty string if no provider available).
156
+ */
157
+ async transcribeVoice(audioUri) {
158
+ const formData = new FormData();
159
+ formData.append('audio', {
160
+ uri: react_native_1.Platform.OS === 'android' ? `file://${audioUri}` : audioUri,
161
+ type: 'audio/wav',
162
+ name: 'voice_input.wav',
163
+ });
164
+ const response = await fetch(`${this.baseUrl}/voice/transcribe`, {
165
+ method: 'POST',
166
+ headers: {
167
+ Authorization: `Bearer ${this.authToken}`,
168
+ },
169
+ body: formData,
170
+ });
171
+ if (!response.ok) {
172
+ const text = await response.text().catch(() => '');
173
+ throw new Error(`[P2PClient] Voice transcribe failed (${response.status}): ${text}`);
174
+ }
175
+ const result = await response.json();
176
+ return {
177
+ text: result.text ?? '',
178
+ provider: result.provider ?? 'none',
179
+ audioFile: result.audioFile,
180
+ };
181
+ }
182
+ /**
183
+ * Trigger a reload of the third-party app.
184
+ * In dev mode, this tells the dev server to hot-reload.
185
+ * In bundle mode, this rebuilds the native bundle and pushes it.
186
+ * The reload command is also broadcast to all connected SDK devices
187
+ * via the BlackBox command channel.
188
+ * @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
189
+ */
190
+ async reloadApp(mode = 'dev') {
191
+ const response = await fetch(`${this.baseUrl}/dev/reload-app`, {
192
+ method: 'POST',
193
+ headers: {
194
+ Authorization: `Bearer ${this.authToken}`,
195
+ 'Content-Type': 'application/json',
196
+ },
197
+ body: JSON.stringify({ mode }),
198
+ });
199
+ if (!response.ok) {
200
+ const text = await response.text().catch(() => '');
201
+ throw new Error(`[P2PClient] Reload app failed (${response.status}): ${text}`);
202
+ }
203
+ return response.json();
204
+ }
205
+ /** Get the download URL for a build artifact. */
206
+ getArtifactUrl(buildId) {
207
+ return `${this.baseUrl}/builds/${buildId}/artifact`;
208
+ }
209
+ /**
210
+ * Start an autonomous test session.
211
+ * The agent reads the codebase for context, then navigates the app
212
+ * on the connected device/emulator, catches exceptions via BlackBox,
213
+ * writes fixes, and hot reloads — all without committing.
214
+ */
215
+ async startTestSession() {
216
+ const response = await fetch(`${this.baseUrl}/test-app/start`, {
217
+ method: 'POST',
218
+ headers: {
219
+ Authorization: `Bearer ${this.authToken}`,
220
+ 'Content-Type': 'application/json',
221
+ },
222
+ body: JSON.stringify({ source: 'feedback-sdk' }),
223
+ });
224
+ if (!response.ok) {
225
+ const text = await response.text().catch(() => '');
226
+ throw new Error(`[P2PClient] Start test session failed (${response.status}): ${text}`);
227
+ }
228
+ return response.json();
229
+ }
230
+ /** Stop a running test session. */
231
+ async stopTestSession() {
232
+ await fetch(`${this.baseUrl}/test-app/stop`, {
233
+ method: 'POST',
234
+ headers: { Authorization: `Bearer ${this.authToken}` },
235
+ });
236
+ }
237
+ /** Get the current test session status and list of fixes. */
238
+ async getTestSession() {
239
+ const response = await this.request('GET', '/test-app/status');
240
+ return response.json();
241
+ }
242
+ /**
243
+ * Rotate the SDK token. The old token stays valid for 5 minutes (grace period).
244
+ * After rotation, the client automatically uses the new token.
245
+ * @returns The new token and its expiry time.
246
+ */
247
+ async rotateToken() {
248
+ const response = await fetch(`${this.baseUrl}/sdk/token/rotate`, {
249
+ method: 'POST',
250
+ headers: {
251
+ Authorization: `Bearer ${this.authToken}`,
252
+ 'Content-Type': 'application/json',
253
+ },
254
+ });
255
+ if (!response.ok) {
256
+ const text = await response.text().catch(() => '');
257
+ throw new Error(`[P2PClient] Token rotation failed (${response.status}): ${text}`);
258
+ }
259
+ const result = await response.json();
260
+ // Auto-update to new token
261
+ this.authToken = result.token;
262
+ return { token: result.token, expiresAt: result.expiresAt };
263
+ }
264
+ // ─── Feature flags (F1) ──────────────────────────────────────────
265
+ /**
266
+ * Evaluate every flag for a userId. Hits /flags/eval which uses
267
+ * SHA256 bucketing against rolloutPercent — stable per user per
268
+ * flag. Results are the dev's source of truth; the SDK caches
269
+ * for 30s in getFlagsCached().
270
+ */
271
+ async flagsEvaluate(userId = 'anonymous') {
272
+ const res = await fetch(`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}`, { headers: { Authorization: `Bearer ${this.authToken}` } });
273
+ if (!res.ok)
274
+ return {};
275
+ const data = await res.json();
276
+ return data.flags ?? {};
277
+ }
278
+ /** Evaluate a single flag by key — shortcut when you only need one. */
279
+ async flagsEvaluateOne(key, userId = 'anonymous') {
280
+ const res = await fetch(`${this.baseUrl}/flags/eval?userId=${encodeURIComponent(userId)}&flag=${encodeURIComponent(key)}`, { headers: { Authorization: `Bearer ${this.authToken}` } });
281
+ if (!res.ok)
282
+ return undefined;
283
+ const data = await res.json();
284
+ return data.value;
285
+ }
286
+ // ─── Releases (R1) ───────────────────────────────────────────────
287
+ /**
288
+ * Ask what bundle this device should run. Returns the latest
289
+ * release in the channel plus a rollout gate. The mobile app
290
+ * uses this on cold start to decide whether to download a new
291
+ * bundle from /releases/bundle.
292
+ */
293
+ async releasesLatest(channel = 'production', deviceId) {
294
+ const params = new URLSearchParams({ channel });
295
+ if (deviceId)
296
+ params.set('device', deviceId);
297
+ const res = await fetch(`${this.baseUrl}/releases/latest?${params.toString()}`, {
298
+ headers: { Authorization: `Bearer ${this.authToken}` },
299
+ });
300
+ if (!res.ok)
301
+ return null;
302
+ return res.json();
303
+ }
304
+ /** Download a specific bundle as raw bytes. */
305
+ async releasesDownload(channel, semver) {
306
+ const params = new URLSearchParams({ channel, semver });
307
+ const res = await fetch(`${this.baseUrl}/releases/bundle?${params.toString()}`, {
308
+ headers: { Authorization: `Bearer ${this.authToken}` },
309
+ });
310
+ if (!res.ok)
311
+ return null;
312
+ return res.arrayBuffer();
313
+ }
314
+ // ─── Analytics ingest (A1 — direct POST path) ───────────────────
315
+ /**
316
+ * Fire-and-forget track event. Most callers should use
317
+ * `BlackBox.track()` which fans through the streaming channel;
318
+ * this method is the fallback for surfaces without a live SSE.
319
+ */
320
+ async analyticsIngest(name, props, opts) {
321
+ try {
322
+ const res = await fetch(`${this.baseUrl}/analytics/ingest`, {
323
+ method: 'POST',
324
+ headers: {
325
+ Authorization: `Bearer ${this.authToken}`,
326
+ 'Content-Type': 'application/json',
327
+ },
328
+ body: JSON.stringify({
329
+ name,
330
+ props,
331
+ deviceId: opts?.deviceId,
332
+ route: opts?.route,
333
+ timestamp: opts?.timestamp ?? Date.now(),
334
+ }),
335
+ });
336
+ return res.ok;
337
+ }
338
+ catch {
339
+ return false;
340
+ }
341
+ }
342
+ /** Internal helper for authenticated GET/POST requests. */
343
+ async request(method, path) {
344
+ const response = await fetch(`${this.baseUrl}${path}`, {
345
+ method,
346
+ headers: {
347
+ Authorization: `Bearer ${this.authToken}`,
348
+ },
349
+ });
350
+ if (!response.ok) {
351
+ const text = await response.text().catch(() => '');
352
+ throw new Error(`[P2PClient] ${method} ${path} failed (${response.status}): ${text}`);
353
+ }
354
+ return response;
355
+ }
356
+ }
357
+ exports.P2PClient = P2PClient;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Detects device shake gestures.
3
+ *
4
+ * Two detection paths run in parallel so shake works in both Debug and
5
+ * Release / TestFlight builds:
6
+ *
7
+ * 1. React Native's built-in `shakeEvent` (iOS) / `ShakeEvent` (Android)
8
+ * on `DeviceEventEmitter`. RN emits `shakeEvent` only in Debug mode
9
+ * on iOS, which is why TestFlight builds never saw a shake prior to
10
+ * SDK 0.5.1.
11
+ * 2. Accelerometer-based fallback via `expo-sensors` (optional peer
12
+ * dep). When the host app has `expo-sensors` installed we subscribe
13
+ * to the accelerometer and fire on a burst of above-threshold peaks.
14
+ * This path works identically in Debug, Release, and TestFlight.
15
+ *
16
+ * Both paths share a 1-second debounce so a single shake never fires the
17
+ * callback twice.
18
+ */
19
+ export declare class ShakeDetector {
20
+ private devMenuSub;
21
+ private accelSub;
22
+ private lastShakeTime;
23
+ private peakTimestamps;
24
+ start(onShake: () => void): void;
25
+ stop(): void;
26
+ private fire;
27
+ /**
28
+ * Dev-menu / platform-native event listener. On iOS this is the
29
+ * `shakeEvent` RN posts from RCTDevMenu (Debug only). On Android it is
30
+ * the `ShakeEvent` name a handful of third-party shake libraries emit.
31
+ */
32
+ private subscribeDevMenu;
33
+ /**
34
+ * Accelerometer-based detection. Uses `expo-sensors` when available —
35
+ * the host app's own dependency, not the SDK's, so apps that don't
36
+ * want the extra native surface get the Dev-menu path only.
37
+ */
38
+ private subscribeAccelerometer;
39
+ }
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ShakeDetector = void 0;
4
+ const react_native_1 = require("react-native");
5
+ // Detects that the surrounding runtime is Yaver's super-host bridge. The
6
+ // YaverInfo native module is only registered inside Yaver's container
7
+ // (see mobile/ios/Yaver/YaverInfo.{swift,m} and the Android equivalent),
8
+ // so it is undefined in a standalone third-party app. When the SDK runs
9
+ // inside Yaver we yield shake handling to Yaver's native
10
+ // ShakeDetectingWindow — the user should only ever see the 2-button
11
+ // "Reload / Back to Yaver" overlay, never a FeedbackModal popped from
12
+ // inside the guest bundle.
13
+ function isRunningInsideYaverHost() {
14
+ try {
15
+ return !!react_native_1.NativeModules?.YaverInfo;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ const SHAKE_TIMEOUT_MS = 1000; // minimum time between shakes
22
+ const ACCEL_THRESHOLD_G = 1.8; // peak g-force that qualifies as a shake event
23
+ const ACCEL_MIN_HITS = 3; // peaks within the window before we fire
24
+ const ACCEL_WINDOW_MS = 800; // rolling window
25
+ const ACCEL_SAMPLE_INTERVAL_MS = 80; // ≈12Hz — cheap but catches shakes
26
+ /**
27
+ * Detects device shake gestures.
28
+ *
29
+ * Two detection paths run in parallel so shake works in both Debug and
30
+ * Release / TestFlight builds:
31
+ *
32
+ * 1. React Native's built-in `shakeEvent` (iOS) / `ShakeEvent` (Android)
33
+ * on `DeviceEventEmitter`. RN emits `shakeEvent` only in Debug mode
34
+ * on iOS, which is why TestFlight builds never saw a shake prior to
35
+ * SDK 0.5.1.
36
+ * 2. Accelerometer-based fallback via `expo-sensors` (optional peer
37
+ * dep). When the host app has `expo-sensors` installed we subscribe
38
+ * to the accelerometer and fire on a burst of above-threshold peaks.
39
+ * This path works identically in Debug, Release, and TestFlight.
40
+ *
41
+ * Both paths share a 1-second debounce so a single shake never fires the
42
+ * callback twice.
43
+ */
44
+ class ShakeDetector {
45
+ constructor() {
46
+ this.devMenuSub = null;
47
+ this.accelSub = null;
48
+ this.lastShakeTime = 0;
49
+ this.peakTimestamps = [];
50
+ }
51
+ start(onShake) {
52
+ this.stop();
53
+ // When the app is loaded inside Yaver's super-host (Hermes push),
54
+ // Yaver owns the shake gesture and shows its own "Reload / Back to
55
+ // Yaver" overlay. We must not also fire the guest-side callback,
56
+ // otherwise the user gets both UIs at once.
57
+ if (isRunningInsideYaverHost())
58
+ return;
59
+ this.subscribeDevMenu(onShake);
60
+ this.subscribeAccelerometer(onShake);
61
+ }
62
+ stop() {
63
+ if (this.devMenuSub) {
64
+ this.devMenuSub.remove();
65
+ this.devMenuSub = null;
66
+ }
67
+ if (this.accelSub) {
68
+ this.accelSub.remove();
69
+ this.accelSub = null;
70
+ }
71
+ this.peakTimestamps = [];
72
+ }
73
+ fire(onShake) {
74
+ const now = Date.now();
75
+ if (now - this.lastShakeTime <= SHAKE_TIMEOUT_MS)
76
+ return;
77
+ this.lastShakeTime = now;
78
+ this.peakTimestamps = [];
79
+ onShake();
80
+ }
81
+ /**
82
+ * Dev-menu / platform-native event listener. On iOS this is the
83
+ * `shakeEvent` RN posts from RCTDevMenu (Debug only). On Android it is
84
+ * the `ShakeEvent` name a handful of third-party shake libraries emit.
85
+ */
86
+ subscribeDevMenu(onShake) {
87
+ const eventName = react_native_1.Platform.OS === 'ios' ? 'shakeEvent' : 'ShakeEvent';
88
+ this.devMenuSub = react_native_1.DeviceEventEmitter.addListener(eventName, () => {
89
+ this.fire(onShake);
90
+ });
91
+ }
92
+ /**
93
+ * Accelerometer-based detection. Uses `expo-sensors` when available —
94
+ * the host app's own dependency, not the SDK's, so apps that don't
95
+ * want the extra native surface get the Dev-menu path only.
96
+ */
97
+ subscribeAccelerometer(onShake) {
98
+ let Accelerometer = null;
99
+ try {
100
+ // Optional peer dep — if it isn't installed, we simply skip this path.
101
+ Accelerometer = require('expo-sensors').Accelerometer;
102
+ }
103
+ catch {
104
+ return;
105
+ }
106
+ if (!Accelerometer)
107
+ return;
108
+ try {
109
+ Accelerometer.setUpdateInterval(ACCEL_SAMPLE_INTERVAL_MS);
110
+ }
111
+ catch {
112
+ // Some platforms reject zero-value intervals; fall through with defaults
113
+ }
114
+ this.accelSub = Accelerometer.addListener(({ x, y, z }) => {
115
+ // Magnitude of acceleration vector (in g). Subtract 1 so a stationary
116
+ // device reports ~0 rather than the 1g of gravity.
117
+ const mag = Math.sqrt(x * x + y * y + z * z);
118
+ if (mag - 1 < ACCEL_THRESHOLD_G - 1)
119
+ return;
120
+ const now = Date.now();
121
+ this.peakTimestamps.push(now);
122
+ // Drop peaks that fell out of the rolling window.
123
+ while (this.peakTimestamps.length > 0 &&
124
+ now - this.peakTimestamps[0] > ACCEL_WINDOW_MS) {
125
+ this.peakTimestamps.shift();
126
+ }
127
+ if (this.peakTimestamps.length >= ACCEL_MIN_HITS) {
128
+ this.fire(onShake);
129
+ }
130
+ });
131
+ }
132
+ }
133
+ exports.ShakeDetector = ShakeDetector;