yaver-feedback-react-native 0.8.13 → 0.9.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.
Files changed (76) hide show
  1. package/app.plugin.js +126 -9
  2. package/dist/BlackBox.js +9 -1
  3. package/dist/DeployPanel.js +65 -0
  4. package/dist/Discovery.js +13 -2
  5. package/dist/FeedbackModal.js +477 -66
  6. package/dist/MachinePickerScreen.js +14 -5
  7. package/dist/P2PClient.d.ts +94 -1
  8. package/dist/P2PClient.js +281 -2
  9. package/dist/ShakeDetector.js +2 -0
  10. package/dist/VibeChatScreen.d.ts +6 -1
  11. package/dist/VibeChatScreen.js +204 -1
  12. package/dist/YaverFeedback.d.ts +98 -0
  13. package/dist/YaverFeedback.js +509 -46
  14. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  15. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  16. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  17. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  18. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  19. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  20. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  21. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  22. package/dist/__tests__/P2PClient.test.js +2 -2
  23. package/dist/__tests__/ReportIdentity.test.d.ts +1 -0
  24. package/dist/__tests__/ReportIdentity.test.js +168 -0
  25. package/dist/__tests__/SDKToken.test.js +1 -1
  26. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  27. package/dist/__tests__/ShakeToggle.test.js +121 -0
  28. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  29. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  30. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  31. package/dist/__tests__/reloadActions.test.js +129 -0
  32. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  33. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  34. package/dist/__tests__/types.test.js +5 -5
  35. package/dist/_core/device.d.ts +19 -9
  36. package/dist/_core/device.js +20 -14
  37. package/dist/capture.d.ts +6 -0
  38. package/dist/capture.js +53 -0
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.js +11 -1
  41. package/dist/reloadActions.d.ts +88 -0
  42. package/dist/reloadActions.js +200 -0
  43. package/dist/storeShots.d.ts +67 -0
  44. package/dist/storeShots.js +137 -0
  45. package/dist/types.d.ts +215 -3
  46. package/dist/voice.d.ts +61 -0
  47. package/dist/voice.js +246 -0
  48. package/package.json +14 -3
  49. package/src/BlackBox.ts +10 -1
  50. package/src/DeployPanel.tsx +74 -0
  51. package/src/Discovery.ts +13 -2
  52. package/src/FeedbackModal.tsx +563 -87
  53. package/src/MachinePickerScreen.tsx +12 -3
  54. package/src/P2PClient.ts +314 -2
  55. package/src/ShakeDetector.ts +1 -0
  56. package/src/VibeChatScreen.tsx +219 -0
  57. package/src/YaverFeedback.ts +498 -46
  58. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  59. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  60. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  61. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  62. package/src/__tests__/P2PClient.test.ts +2 -2
  63. package/src/__tests__/ReportIdentity.test.ts +203 -0
  64. package/src/__tests__/SDKToken.test.ts +1 -1
  65. package/src/__tests__/ShakeToggle.test.ts +153 -0
  66. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  67. package/src/__tests__/reloadActions.test.ts +171 -0
  68. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  69. package/src/__tests__/types.test.ts +5 -5
  70. package/src/_core/device.ts +20 -14
  71. package/src/capture.ts +51 -0
  72. package/src/index.ts +21 -0
  73. package/src/reloadActions.ts +273 -0
  74. package/src/storeShots.ts +189 -0
  75. package/src/types.ts +217 -3
  76. package/src/voice.ts +270 -0
@@ -102,7 +102,14 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
102
102
  return;
103
103
  }
104
104
  const direct = await probeDeviceReachability(device);
105
- if (!direct.reachable && !device.needsAuth) {
105
+ // Do not hard-block selection just because the LAN /health probe
106
+ // failed. The standalone SDK can still reach a healthy machine via
107
+ // the normal selected-device discovery path (including relay), and
108
+ // the Yaver host path may already be proving the machine works.
109
+ // Only treat the machine as unpickable when BOTH:
110
+ // 1. Convex says it is offline, and
111
+ // 2. the direct probe also failed.
112
+ if (!device.isOnline && !direct.reachable && !device.needsAuth) {
106
113
  setError('Selected machine is not responding. Start `yaver serve` on it and try again.');
107
114
  setReachability((prev) => ({ ...prev, [device.deviceId]: direct }));
108
115
  return;
@@ -130,7 +137,9 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
130
137
  ? '#f59e0b'
131
138
  : effectivelyReachable
132
139
  ? '#22c55e'
133
- : explicitlyOffline || !device.isOnline
140
+ : device.isOnline
141
+ ? '#f59e0b'
142
+ : explicitlyOffline || !device.isOnline
134
143
  ? '#ef4444'
135
144
  : '#22c55e';
136
145
  // Derive a single short status phrase the user can act on.
@@ -145,7 +154,7 @@ export const YaverMachinePickerScreen: React.FC<YaverMachinePickerProps> = ({
145
154
  statusLine =
146
155
  'Needs pairing — open the Yaver app to adopt this machine';
147
156
  } else if (explicitlyOffline) {
148
- statusLine = 'Agent not responding on this machine';
157
+ statusLine = 'Online, but direct probe failed — relay / selected-machine path may still work';
149
158
  } else if (device.runnerDown) {
150
159
  statusLine = 'Runner down — restart the coding agent on the Mac';
151
160
  } else {
package/src/P2PClient.ts CHANGED
@@ -1,13 +1,27 @@
1
1
  import { Platform } from 'react-native';
2
2
  import {
3
+ AppInfo,
3
4
  CapabilitySnapshot,
4
5
  FeedbackBundle,
6
+ FeedbackProjectRef,
5
7
  IncidentEvent,
8
+ OpenCodeConfigSummary,
6
9
  OperationState,
7
10
  RunnerBrowserAuthSession,
11
+ RunnerAuthStatusRow,
8
12
  TestSession,
9
13
  VoiceCapability,
10
14
  } from './types';
15
+ import { AGENT_ENDPOINTS } from './_core/endpoints';
16
+ import { RELOAD_PATH, describeReloadFailure } from './reloadActions';
17
+ import type { DevServerSnapshot, ReloadWireMode } from './reloadActions';
18
+
19
+ function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
20
+ const maybeNodeTimer = timer as unknown as { unref?: () => void };
21
+ if (typeof maybeNodeTimer.unref === 'function') {
22
+ maybeNodeTimer.unref();
23
+ }
24
+ }
11
25
 
12
26
  export interface FeedbackEvent {
13
27
  type: string;
@@ -74,6 +88,148 @@ export function resolveAppIdentity(opts?: {
74
88
  return out;
75
89
  }
76
90
 
91
+ /**
92
+ * Is this JS running inside Yaver's own container as a pushed Hermes guest?
93
+ *
94
+ * The YaverInfo native module is registered only by Yaver's app, so its
95
+ * presence means the ambient runtime describes Yaver rather than the app
96
+ * whose code is executing. Mirrors the checks in YaverFeedback.ts,
97
+ * ShakeDetector.ts and QuickActionIcon.tsx.
98
+ */
99
+ function isInsideYaverContainer(): boolean {
100
+ try {
101
+ const { NativeModules } = require('react-native');
102
+ return !!NativeModules?.YaverInfo;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Read the guest project Yaver's host shell pinned for this bundle.
110
+ *
111
+ * When an app's JS is pushed into Yaver's container as a Hermes guest, the
112
+ * ambient runtime describes the HOST — `expo-constants` and `SettingsManager`
113
+ * both answer `Yaver` / `io.yaver.mobile`. Auto-resolution would therefore
114
+ * file every guest report against Yaver's own repo, and the fix task would
115
+ * edit the SDK instead of the app under test.
116
+ *
117
+ * Yaver already knows the right answer: picking a project in the Hot Reload
118
+ * tab calls `YaverInfo.setInheritedGuestProject(name, path)`. Only the name
119
+ * is used here — the agent resolves the path itself and deliberately ignores
120
+ * client-supplied ones on feedback reports.
121
+ */
122
+ function resolveInheritedGuestProjectName(): string | undefined {
123
+ try {
124
+ const { NativeModules } = require('react-native');
125
+ const name = String(NativeModules?.YaverInfo?.inheritedGuestProjectName || '').trim();
126
+ return name || undefined;
127
+ } catch {
128
+ // Not inside Yaver's container, or react-native unavailable (unit tests).
129
+ return undefined;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Build the app-identity half of a feedback report's metadata.
135
+ *
136
+ * `resolveAppIdentity()` has always fed /vibing/execute and /dev/reload-app,
137
+ * so the agent could route a "vibe on THIS app" request to the right repo —
138
+ * but nothing fed /feedback. Reports arrived with no identity at all, so the
139
+ * agent's fix router fell through to its own working directory and edited
140
+ * whichever repo it happened to be sitting in. This closes that gap by
141
+ * reusing the same resolver for the feedback path.
142
+ *
143
+ * Precedence, most to least trustworthy:
144
+ * 1. What the host app declared (`FeedbackConfig.projectName`/`bundleId`).
145
+ * An app knows its own identity; nothing should override it.
146
+ * 2. The guest project Yaver's host shell pinned, when running as a Hermes
147
+ * guest. Ambient lookups describe Yaver there, not the guest.
148
+ * 3. Ambient expo-constants / native modules — correct for a standalone
149
+ * build, which is the common case.
150
+ *
151
+ * Every lookup is best-effort: a bare RN app with no expo-constants still
152
+ * produces a report, just one the agent resolves by its own means.
153
+ */
154
+ export function resolveReportIdentity(opts?: {
155
+ projectName?: string;
156
+ bundleId?: string;
157
+ surface?: FeedbackProjectRef['surface'];
158
+ surfaces?: FeedbackProjectRef['surfaces'];
159
+ stack?: string;
160
+ stacks?: string[];
161
+ testSurfaces?: string[];
162
+ feedbackSdk?: string;
163
+ feedbackTransport?: string;
164
+ voiceCapabilities?: string[];
165
+ sttProvider?: string;
166
+ ttsProvider?: string;
167
+ }): {
168
+ appName?: string;
169
+ app: AppInfo;
170
+ project?: FeedbackProjectRef;
171
+ } {
172
+ let projectName = (opts?.projectName || '').trim() || undefined;
173
+ let bundleId = (opts?.bundleId || '').trim() || undefined;
174
+ let version: string | undefined;
175
+ let buildNumber: string | undefined;
176
+
177
+ if (isInsideYaverContainer()) {
178
+ // Every ambient lookup answers for the HOST here. Because the agent
179
+ // routes on bundle id first, letting Yaver's id through would resolve
180
+ // straight to Yaver's own repo and the guest's name would never be
181
+ // consulted — so nothing ambient is trusted in the container. The
182
+ // version is Yaver's too, and reporting it would only mislabel which
183
+ // build the report came from.
184
+ projectName = projectName || resolveInheritedGuestProjectName();
185
+ } else {
186
+ const ambient = resolveAppIdentity({ projectName, bundleId });
187
+ projectName = ambient.projectName;
188
+ bundleId = ambient.bundleId;
189
+ try {
190
+ const Constants = require('expo-constants').default ?? require('expo-constants');
191
+ const cfg = Constants?.expoConfig ?? Constants?.manifest ?? {};
192
+ version = Constants?.nativeAppVersion || cfg?.version;
193
+ buildNumber =
194
+ Constants?.nativeBuildVersion ||
195
+ cfg?.ios?.buildNumber ||
196
+ (cfg?.android?.versionCode != null ? String(cfg.android.versionCode) : undefined);
197
+ } catch {
198
+ // expo-constants not installed (bare RN). Version is cosmetic — the
199
+ // router keys off appName/bundleId, both of which survive without it.
200
+ }
201
+ }
202
+
203
+ const app: AppInfo = {};
204
+ if (bundleId) app.bundleId = bundleId;
205
+ if (version) app.version = version;
206
+ if (buildNumber) app.buildNumber = buildNumber;
207
+
208
+ if (!projectName && !bundleId) {
209
+ return { app };
210
+ }
211
+
212
+ const project: FeedbackProjectRef = { surface: opts?.surface ?? 'mobile' };
213
+ if (projectName) {
214
+ project.projectName = projectName;
215
+ project.appName = projectName;
216
+ }
217
+ // Note: projectPath is intentionally omitted — the agent ignores
218
+ // client-supplied paths on feedback reports and resolves them itself.
219
+ if (bundleId) project.bundleId = bundleId;
220
+ if (opts?.surfaces) project.surfaces = opts.surfaces;
221
+ if (opts?.stack) project.stack = opts.stack;
222
+ if (opts?.stacks) project.stacks = opts.stacks;
223
+ if (opts?.testSurfaces) project.testSurfaces = opts.testSurfaces;
224
+ if (opts?.feedbackSdk) project.feedbackSdk = opts.feedbackSdk;
225
+ if (opts?.feedbackTransport) project.feedbackTransport = opts.feedbackTransport;
226
+ if (opts?.voiceCapabilities) project.voiceCapabilities = opts.voiceCapabilities;
227
+ if (opts?.sttProvider) project.sttProvider = opts.sttProvider;
228
+ if (opts?.ttsProvider) project.ttsProvider = opts.ttsProvider;
229
+
230
+ return { appName: projectName, app, project };
231
+ }
232
+
77
233
  /**
78
234
  * Translate a raw Go-agent error into something a user can act on.
79
235
  *
@@ -154,6 +310,25 @@ export class P2PClient {
154
310
  this.relayPassword = password;
155
311
  }
156
312
 
313
+ /** Read-only base URL — used by the voice vibe-coding path to probe
314
+ * GET /voice/status before opening the stream. */
315
+ get agentBaseUrl(): string {
316
+ return this.baseUrl;
317
+ }
318
+
319
+ /** WebSocket URL for the agent's voice stream (WS /voice/stream). The
320
+ * voice vibe-coding loop streams mic audio here and receives the
321
+ * transcript + agent task + TTS frames back. */
322
+ voiceStreamUrl(): string {
323
+ return this.baseUrl.replace(/^http/, 'ws') + '/voice/stream';
324
+ }
325
+
326
+ /** Auth headers for the voice WS + status probe — same bearer (and
327
+ * relay password) as every other agent request. */
328
+ voiceAuthHeaders(): Record<string, string> {
329
+ return this.authHeaders();
330
+ }
331
+
157
332
  /** Merge in Authorization + (optional) X-Relay-Password on top of a header block. */
158
333
  private authHeaders(extra: Record<string, string> = {}): Record<string, string> {
159
334
  const h: Record<string, string> = { ...extra };
@@ -222,6 +397,60 @@ export class P2PClient {
222
397
  return data.session as RunnerBrowserAuthSession;
223
398
  }
224
399
 
400
+ async getRunnerAuthStatus(): Promise<RunnerAuthStatusRow[]> {
401
+ const resp = await fetch(`${this.baseUrl}/runner-auth/status`, {
402
+ headers: this.authHeaders(),
403
+ });
404
+ if (!resp.ok) {
405
+ const text = await resp.text().catch(() => '');
406
+ throw new Error(`getRunnerAuthStatus HTTP ${resp.status}: ${text}`);
407
+ }
408
+ const data = await resp.json().catch(() => ({} as Record<string, unknown>));
409
+ return Array.isArray(data.runners) ? (data.runners as RunnerAuthStatusRow[]) : [];
410
+ }
411
+
412
+ async getOpenCodeConfig(): Promise<OpenCodeConfigSummary | null> {
413
+ const resp = await fetch(`${this.baseUrl}/runner/opencode/config`, {
414
+ headers: this.authHeaders(),
415
+ });
416
+ if (!resp.ok) {
417
+ const text = await resp.text().catch(() => '');
418
+ throw new Error(`getOpenCodeConfig HTTP ${resp.status}: ${text}`);
419
+ }
420
+ const data = await resp.json().catch(() => ({} as Record<string, unknown>));
421
+ return (data.config ?? null) as OpenCodeConfigSummary | null;
422
+ }
423
+
424
+ async saveOpenCodeConfig(patch: {
425
+ defaultAgent?: string;
426
+ model?: string;
427
+ smallModel?: string;
428
+ buildModel?: string;
429
+ planModel?: string;
430
+ providers?: Array<{
431
+ id: string;
432
+ name?: string;
433
+ baseUrl?: string;
434
+ apiKey?: string;
435
+ delete?: boolean;
436
+ }>;
437
+ }): Promise<{ ok: boolean; config?: OpenCodeConfigSummary; error?: string }> {
438
+ try {
439
+ const resp = await fetch(`${this.baseUrl}/runner/opencode/config`, {
440
+ method: 'POST',
441
+ headers: { ...this.authHeaders(), 'Content-Type': 'application/json' },
442
+ body: JSON.stringify(patch),
443
+ });
444
+ const data = await resp.json().catch(() => ({} as Record<string, unknown>));
445
+ if (!resp.ok) {
446
+ return { ok: false, error: (data.error as string | undefined) || `HTTP ${resp.status}` };
447
+ }
448
+ return { ok: true, config: data.config as OpenCodeConfigSummary | undefined };
449
+ } catch (err) {
450
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
451
+ }
452
+ }
453
+
225
454
  async capabilitySnapshot(): Promise<CapabilitySnapshot | null> {
226
455
  try {
227
456
  const resp = await fetch(`${this.baseUrl}/capabilities/snapshot`, { headers: this.authHeaders() });
@@ -285,19 +514,22 @@ export class P2PClient {
285
514
 
286
515
  /** Health check — returns true if the agent is reachable. */
287
516
  async health(): Promise<boolean> {
517
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
288
518
  try {
289
519
  const controller = new AbortController();
290
- const timeoutId = setTimeout(() => controller.abort(), 3000);
520
+ timeoutId = setTimeout(() => controller.abort(), 3000);
521
+ unrefTimer(timeoutId);
291
522
 
292
523
  const response = await fetch(`${this.baseUrl}/health`, {
293
524
  method: 'GET',
294
525
  signal: controller.signal,
295
526
  });
296
527
 
297
- clearTimeout(timeoutId);
298
528
  return response.ok;
299
529
  } catch {
300
530
  return false;
531
+ } finally {
532
+ if (timeoutId) clearTimeout(timeoutId);
301
533
  }
302
534
  }
303
535
 
@@ -422,6 +654,10 @@ export class P2PClient {
422
654
  s2sReady: data.s2sReady ?? false,
423
655
  sttProvider: data.sttProvider ?? undefined,
424
656
  sttReady: data.sttReady ?? false,
657
+ ttsProvider: data.ttsProvider ?? undefined,
658
+ ttsReady: data.ttsReady ?? false,
659
+ enabled: data.enabled ?? false,
660
+ defaultProject: data.defaultProject ?? undefined,
425
661
  };
426
662
  }
427
663
 
@@ -466,6 +702,82 @@ export class P2PClient {
466
702
  * via the BlackBox command channel.
467
703
  * @param mode - "dev" for hot reload, "bundle" for native bundle rebuild
468
704
  */
705
+ /**
706
+ * Read the dev server's state so the overlay can decide WHICH reload
707
+ * actions to offer, and disable the rest with a reason.
708
+ *
709
+ * Returns null when the machine cannot be reached at all — which the
710
+ * caller must render as "not connected", never as "no dev server".
711
+ * Those are two different problems with two different fixes.
712
+ */
713
+ async getDevServerStatus(): Promise<DevServerSnapshot | null> {
714
+ try {
715
+ const resp = await fetch(`${this.baseUrl}${AGENT_ENDPOINTS.devStatus}`, {
716
+ headers: this.authHeaders(),
717
+ });
718
+ if (!resp.ok) return null;
719
+ const data = await resp.json().catch(() => ({} as Record<string, unknown>));
720
+ return {
721
+ running: data.running === true,
722
+ building: data.building === true,
723
+ framework: typeof data.framework === 'string' ? data.framework : undefined,
724
+ };
725
+ } catch {
726
+ return null;
727
+ }
728
+ }
729
+
730
+ /**
731
+ * Trigger a reload with an EXPLICIT fast/full mode — no bundle fallback.
732
+ *
733
+ * `reloadApp('dev')` silently falls through to a bundle rebuild when the
734
+ * dev server is down, which is right for a one-button UX and wrong the
735
+ * moment the user picks between two named actions: someone who pressed
736
+ * "Full Reload" must not get a 60-second bundle rebuild without being
737
+ * told. So this method reports the failure instead, with a named cause.
738
+ *
739
+ * Auth: the SAME bearer used for the feedback POST. `/dev/reload` is
740
+ * registered under `authSDKOrGuest` in desktop/agent/httpserver.go and is
741
+ * already in the `guest-reload` SDK-token scope list — nothing widens.
742
+ */
743
+ async reloadWithMode(
744
+ mode: ReloadWireMode,
745
+ snapshot?: DevServerSnapshot | null,
746
+ ): Promise<ReloadAck> {
747
+ if (mode === 'bundle') return this.reloadApp('bundle');
748
+
749
+ let resp: Response;
750
+ try {
751
+ resp = await fetch(`${this.baseUrl}${RELOAD_PATH}`, {
752
+ method: 'POST',
753
+ headers: this.authHeaders({ 'Content-Type': 'application/json' }),
754
+ body: JSON.stringify({ mode }),
755
+ });
756
+ } catch (err) {
757
+ throw new Error(
758
+ describeReloadFailure(0, err instanceof Error ? err.message : '', snapshot),
759
+ );
760
+ }
761
+ if (!resp.ok) {
762
+ const text = await resp.text().catch(() => '');
763
+ throw new Error(describeReloadFailure(resp.status, text, snapshot));
764
+ }
765
+ const payload = await resp.json().catch(() => ({} as Record<string, unknown>));
766
+ const nativeChangesDetected = payload.nativeChangesDetected === true;
767
+ return {
768
+ ok: true,
769
+ mode: 'dev',
770
+ acknowledged: true,
771
+ nativeChangesDetected,
772
+ changeClass: typeof payload.changeClass === 'string' ? payload.changeClass : undefined,
773
+ message: nativeChangesDetected
774
+ ? 'Reload accepted, but native files changed — a rebuild is required.'
775
+ : mode === 'full'
776
+ ? 'Full reload requested.'
777
+ : 'Hot reload requested.',
778
+ };
779
+ }
780
+
469
781
  async reloadApp(
470
782
  mode: 'dev' | 'bundle' = 'bundle',
471
783
  opts?: { projectName?: string; bundleId?: string; projectPath?: string },
@@ -83,6 +83,7 @@ export class ShakeDetector {
83
83
  * the `ShakeEvent` name a handful of third-party shake libraries emit.
84
84
  */
85
85
  private subscribeDevMenu(onShake: () => void): void {
86
+ if (typeof DeviceEventEmitter?.addListener !== 'function') return;
86
87
  const eventName = Platform.OS === 'ios' ? 'shakeEvent' : 'ShakeEvent';
87
88
  this.devMenuSub = DeviceEventEmitter.addListener(eventName, () => {
88
89
  this.fire(onShake);