yaver-feedback-react-native 0.9.0 → 0.9.3

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 (144) hide show
  1. package/README.md +102 -1
  2. package/app.plugin.js +126 -9
  3. package/dist/AuthOverlay.d.ts +1 -14
  4. package/dist/AuthOverlay.js +9 -62
  5. package/dist/BlackBox.js +9 -1
  6. package/dist/DeployPanel.js +65 -0
  7. package/dist/Discovery.js +13 -8
  8. package/dist/DogfoodRuntime.d.ts +124 -0
  9. package/dist/DogfoodRuntime.js +273 -0
  10. package/dist/FeedbackModal.js +505 -65
  11. package/dist/LoginScreen.d.ts +1 -5
  12. package/dist/LoginScreen.js +2 -6
  13. package/dist/MachinePickerScreen.d.ts +1 -3
  14. package/dist/MachinePickerScreen.js +6 -18
  15. package/dist/P2PClient.d.ts +178 -2
  16. package/dist/P2PClient.js +493 -3
  17. package/dist/P2PDogfoodDriver.d.ts +12 -0
  18. package/dist/P2PDogfoodDriver.js +118 -0
  19. package/dist/PairDeviceModal.d.ts +2 -3
  20. package/dist/ShakeDetector.js +2 -0
  21. package/dist/VibeChatScreen.d.ts +11 -1
  22. package/dist/VibeChatScreen.js +200 -41
  23. package/dist/YaverFeedback.d.ts +132 -0
  24. package/dist/YaverFeedback.js +633 -65
  25. package/dist/YaverModeBadge.d.ts +22 -0
  26. package/dist/YaverModeBadge.js +219 -0
  27. package/dist/__tests__/AuthDevices.test.js +1 -48
  28. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  29. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  30. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  31. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  32. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  33. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  34. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  35. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  36. package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
  37. package/dist/__tests__/DogfoodRuntime.test.js +116 -0
  38. package/dist/__tests__/P2PClient.test.js +2 -2
  39. package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
  40. package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
  41. package/dist/__tests__/ReportIdentity.test.d.ts +25 -0
  42. package/dist/__tests__/ReportIdentity.test.js +180 -0
  43. package/dist/__tests__/SDKToken.test.js +1 -1
  44. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  45. package/dist/__tests__/ShakeToggle.test.js +121 -0
  46. package/dist/__tests__/YaverFeedback.test.js +13 -3
  47. package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
  48. package/dist/__tests__/deviceDogfood.test.js +86 -0
  49. package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
  50. package/dist/__tests__/dogfoodPolicy.test.js +33 -0
  51. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  52. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  53. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  54. package/dist/__tests__/reloadActions.test.js +129 -0
  55. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  56. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  57. package/dist/__tests__/types.test.js +5 -5
  58. package/dist/_core/ansi.d.ts +117 -0
  59. package/dist/_core/ansi.js +468 -0
  60. package/dist/_core/ansi.test.d.ts +1 -0
  61. package/dist/_core/ansi.test.js +225 -0
  62. package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
  63. package/dist/_core/buildFeedbackPrompt.js +18 -72
  64. package/dist/_core/constants.d.ts +19 -6
  65. package/dist/_core/constants.js +20 -7
  66. package/dist/_core/device.d.ts +1 -5
  67. package/dist/_core/device.js +5 -14
  68. package/dist/_core/endpoints.d.ts +0 -7
  69. package/dist/_core/endpoints.js +0 -7
  70. package/dist/_core/index.d.ts +4 -0
  71. package/dist/_core/index.js +4 -0
  72. package/dist/_core/remoteless.d.ts +44 -0
  73. package/dist/_core/remoteless.js +75 -0
  74. package/dist/_core/trace.d.ts +47 -0
  75. package/dist/_core/trace.js +38 -0
  76. package/dist/_core/trace.test.d.ts +1 -0
  77. package/dist/_core/trace.test.js +60 -0
  78. package/dist/auth.d.ts +3 -60
  79. package/dist/auth.js +6 -88
  80. package/dist/deviceDogfood.d.ts +53 -0
  81. package/dist/deviceDogfood.js +137 -0
  82. package/dist/dogfoodPolicy.d.ts +25 -0
  83. package/dist/dogfoodPolicy.js +24 -0
  84. package/dist/index.d.ts +17 -4
  85. package/dist/index.js +33 -7
  86. package/dist/reloadActions.d.ts +88 -0
  87. package/dist/reloadActions.js +200 -0
  88. package/dist/storeShots.d.ts +67 -0
  89. package/dist/storeShots.js +137 -0
  90. package/dist/types.d.ts +208 -8
  91. package/package.json +13 -4
  92. package/src/AuthOverlay.tsx +20 -106
  93. package/src/BlackBox.ts +10 -1
  94. package/src/DeployPanel.tsx +74 -0
  95. package/src/Discovery.ts +13 -8
  96. package/src/DogfoodRuntime.ts +373 -0
  97. package/src/FeedbackModal.tsx +619 -79
  98. package/src/LoginScreen.tsx +2 -22
  99. package/src/MachinePickerScreen.tsx +6 -21
  100. package/src/P2PClient.ts +580 -4
  101. package/src/P2PDogfoodDriver.ts +132 -0
  102. package/src/PairDeviceModal.tsx +2 -3
  103. package/src/ShakeDetector.ts +1 -0
  104. package/src/VibeChatScreen.tsx +232 -42
  105. package/src/YaverFeedback.ts +631 -68
  106. package/src/YaverModeBadge.tsx +234 -0
  107. package/src/__tests__/AuthDevices.test.ts +1 -52
  108. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  109. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  110. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  111. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  112. package/src/__tests__/DogfoodRuntime.test.ts +135 -0
  113. package/src/__tests__/P2PClient.test.ts +2 -2
  114. package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
  115. package/src/__tests__/ReportIdentity.test.ts +212 -0
  116. package/src/__tests__/SDKToken.test.ts +1 -1
  117. package/src/__tests__/ShakeToggle.test.ts +153 -0
  118. package/src/__tests__/YaverFeedback.test.ts +15 -3
  119. package/src/__tests__/deviceDogfood.test.ts +77 -0
  120. package/src/__tests__/dogfoodPolicy.test.ts +34 -0
  121. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  122. package/src/__tests__/reloadActions.test.ts +171 -0
  123. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  124. package/src/__tests__/types.test.ts +5 -5
  125. package/src/_core/ansi.test.ts +250 -0
  126. package/src/_core/ansi.ts +475 -0
  127. package/src/_core/buildFeedbackPrompt.ts +18 -95
  128. package/src/_core/constants.ts +20 -6
  129. package/src/_core/device.ts +5 -16
  130. package/src/_core/endpoints.ts +0 -7
  131. package/src/_core/index.ts +4 -0
  132. package/src/_core/remoteless.ts +110 -0
  133. package/src/_core/trace.test.ts +58 -0
  134. package/src/_core/trace.ts +75 -0
  135. package/src/auth.ts +7 -156
  136. package/src/deviceDogfood.ts +171 -0
  137. package/src/dogfoodPolicy.ts +40 -0
  138. package/src/index.ts +61 -11
  139. package/src/reloadActions.ts +273 -0
  140. package/src/storeShots.ts +189 -0
  141. package/src/types.ts +209 -8
  142. package/dist/GuestOnboardingScreen.d.ts +0 -8
  143. package/dist/GuestOnboardingScreen.js +0 -282
  144. package/src/GuestOnboardingScreen.tsx +0 -307
@@ -0,0 +1,234 @@
1
+ // YaverModeBadge.tsx — the polite "you're running inside Yaver" mark, for
2
+ // THIRD-PARTY apps.
3
+ //
4
+ // ── Why a third-party app needs this ────────────────────────────────────────
5
+ //
6
+ // When your app is pushed into Yaver's container as a Hermes bundle, it looks
7
+ // exactly like your installed build. Same icon, same launch, same everything —
8
+ // except it is a development bundle from someone's branch. Testers have chased
9
+ // "bugs" that were only unbuilt work, and have been unable to find their way
10
+ // back to the real app because nothing on screen said there was anywhere to go.
11
+ //
12
+ // So the SDK shows one small Y, low contrast, in a corner. Tapping it says what
13
+ // you're in and how to leave. Nothing else.
14
+ //
15
+ // ── Defaults, and why ───────────────────────────────────────────────────────
16
+ //
17
+ // DEFAULT ENABLED (`modeBadge: true`), because the failure it prevents is
18
+ // silent and lands on a tester rather than the developer — exactly the class
19
+ // that should not be opt-in. Turn it off with `modeBadge: false` when your app
20
+ // has its own in-container indicator, or when a screenshot-perfect surface
21
+ // genuinely cannot afford 22 points in a corner.
22
+ //
23
+ // ── What it is NOT ──────────────────────────────────────────────────────────
24
+ //
25
+ // It is NOT the escape. Yaver's container owns that: shake opens Yaver's own
26
+ // overlay with "Back to Yaver". This badge only tells you the gesture exists.
27
+ // Making it the exit would put the way out inside the previewed app, which is
28
+ // the trap the container's escape-ownership rules exist to prevent — the guest
29
+ // could style over it, or unmount it, and the tester would be stuck.
30
+
31
+ import React, { useEffect, useState } from 'react';
32
+ import { Alert, Modal, NativeModules, Pressable, Text, View } from 'react-native';
33
+
34
+ /** True only inside Yaver's container: the YaverInfo native module is
35
+ * registered by Yaver's app and by nothing else. Same probe ShakeDetector
36
+ * and P2PClient use — one answer, not three. */
37
+ function isInsideYaver(): boolean {
38
+ try {
39
+ return !!(NativeModules as any)?.YaverInfo;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Per-RUN dismissal, shared by every mounted badge.
47
+ *
48
+ * Deliberately in memory, not storage. A permanently hidden badge recreates
49
+ * exactly the problem the badge exists to prevent: a tester who cannot tell an
50
+ * unbuilt branch from the installed app, and cannot find the way back. Polite
51
+ * means not nagging within a session — it does not mean permanent amnesia
52
+ * about which build you are looking at. A relaunch is a new context.
53
+ *
54
+ * An APP that wants it gone for good passes `modeBadge: false` at init. That is
55
+ * a developer making an informed choice, which is a different thing from a
56
+ * tester clearing their screen for a minute.
57
+ */
58
+ let hiddenThisRun = false;
59
+ const hideListeners = new Set<() => void>();
60
+
61
+ /** Hide the mark for the rest of this run. Exposed on YaverFeedback so a host
62
+ * can wire its own "hide" control, and used by the sheet's own button. */
63
+ export function hideYaverModeBadge(): void {
64
+ hiddenThisRun = true;
65
+ hideListeners.forEach((l) => {
66
+ try {
67
+ l();
68
+ } catch {
69
+ /* one bad listener mustn't block the others */
70
+ }
71
+ });
72
+ }
73
+
74
+ /** Bring it back — e.g. when a NEW guest bundle loads and the context changed. */
75
+ export function showYaverModeBadge(): void {
76
+ hiddenThisRun = false;
77
+ hideListeners.forEach((l) => {
78
+ try {
79
+ l();
80
+ } catch {
81
+ /* noop */
82
+ }
83
+ });
84
+ }
85
+
86
+ export function isYaverModeBadgeHidden(): boolean {
87
+ return hiddenThisRun;
88
+ }
89
+
90
+ export interface YaverModeBadgeProps {
91
+ /** Corner to sit in. Default bottom-left: bottom-right is where most apps
92
+ * put a FAB, and the badge must never compete with the app's own action. */
93
+ position?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
94
+ /** Render even outside Yaver. Development aid only — the badge is meaningless
95
+ * in a standalone build and would be wallpaper. */
96
+ force?: boolean;
97
+ /** Account-scoped third-party Dogfood mode. Keeps the same Yaver Y mark but
98
+ * changes the explanation and offers a confirmed exit to normal Feedback. */
99
+ dogfood?: boolean;
100
+ dogfoodLabel?: string;
101
+ onExitDogfood?: () => void | Promise<void>;
102
+ }
103
+
104
+ export function YaverModeBadge({
105
+ position = 'bottom-left',
106
+ force = false,
107
+ dogfood = false,
108
+ dogfoodLabel = 'Dogfood',
109
+ onExitDogfood,
110
+ }: YaverModeBadgeProps) {
111
+ const [open, setOpen] = useState(false);
112
+ const [, bump] = useState(0);
113
+
114
+ useEffect(() => {
115
+ const listener = () => bump((n) => n + 1);
116
+ hideListeners.add(listener);
117
+ return () => {
118
+ hideListeners.delete(listener);
119
+ };
120
+ }, []);
121
+
122
+ // Standalone builds render nothing at all — zero cost, zero pixels.
123
+ if (!force && !dogfood && !isInsideYaver()) return null;
124
+ if (hiddenThisRun) return null;
125
+
126
+ const vertical = position.startsWith('top') ? { top: 44 } : { bottom: 28 };
127
+ const horizontal = position.endsWith('left') ? { left: 12 } : { right: 12 };
128
+
129
+ return (
130
+ <>
131
+ <View
132
+ // The wrapper never intercepts touches; only the 22pt mark does. A
133
+ // full-screen overlay that swallowed taps would break the app it is
134
+ // supposed to be politely annotating.
135
+ pointerEvents="box-none"
136
+ style={{ position: 'absolute', ...vertical, ...horizontal, zIndex: 9998 }}
137
+ >
138
+ <Pressable
139
+ onPress={() => setOpen(true)}
140
+ accessibilityRole="button"
141
+ accessibilityLabel={dogfood ? `Exit ${dogfoodLabel} Dogfood mode` : 'Running inside Yaver'}
142
+ hitSlop={10}
143
+ style={({ pressed }) => ({
144
+ width: 22,
145
+ height: 22,
146
+ borderRadius: 11,
147
+ alignItems: 'center',
148
+ justifyContent: 'center',
149
+ borderWidth: 1,
150
+ borderColor: 'rgba(124,92,255,0.45)',
151
+ backgroundColor: 'rgba(124,92,255,0.14)',
152
+ opacity: pressed ? 0.6 : 0.9,
153
+ })}
154
+ >
155
+ <Text style={{ color: '#7C5CFF', fontSize: 12, fontWeight: '700', lineHeight: 14 }}>Y</Text>
156
+ </Pressable>
157
+ </View>
158
+
159
+ <Modal visible={open} transparent animationType="fade" onRequestClose={() => setOpen(false)}>
160
+ <Pressable style={{ flex: 1, backgroundColor: '#0008' }} onPress={() => setOpen(false)}>
161
+ <Pressable
162
+ onPress={(e) => e.stopPropagation()}
163
+ style={{
164
+ margin: 24,
165
+ marginTop: 'auto',
166
+ marginBottom: 48,
167
+ borderRadius: 16,
168
+ padding: 18,
169
+ backgroundColor: '#151519',
170
+ borderWidth: 1,
171
+ borderColor: '#2a2a32',
172
+ gap: 10,
173
+ }}
174
+ >
175
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
176
+ <Text style={{ color: '#7C5CFF', fontSize: 15, fontWeight: '800' }}>Y</Text>
177
+ <Text style={{ color: '#fff', fontSize: 15, fontWeight: '700' }}>
178
+ {dogfood ? `${dogfoodLabel} Dogfood mode` : 'Running inside Yaver'}
179
+ </Text>
180
+ </View>
181
+ <Text style={{ color: '#b6b6c0', fontSize: 13, lineHeight: 19 }}>
182
+ {dogfood
183
+ ? 'This approved account is improving the app directly through Yaver. Tasks and re-render actions target this app instead of creating a feedback report.'
184
+ : 'This is a development build loaded into Yaver — not the version installed on this device. Anything unfinished here is work in progress, not a released bug.'}
185
+ </Text>
186
+ {dogfood ? (
187
+ <Pressable
188
+ onPress={() => Alert.alert(
189
+ 'Exit Dogfood mode?',
190
+ 'Return this SDK to normal Feedback mode. Your coding tasks and source changes are untouched.',
191
+ [
192
+ { text: 'Keep Dogfooding', style: 'cancel' },
193
+ { text: 'Exit Dogfood', style: 'destructive', onPress: () => {
194
+ void onExitDogfood?.();
195
+ setOpen(false);
196
+ } },
197
+ ],
198
+ )}
199
+ style={{ paddingVertical: 10, alignItems: 'center' }}
200
+ >
201
+ <Text style={{ color: '#fca5a5', fontSize: 13, fontWeight: '700' }}>Exit Dogfood mode</Text>
202
+ </Pressable>
203
+ ) : (
204
+ <Text style={{ color: '#b6b6c0', fontSize: 13, lineHeight: 19 }}>
205
+ Shake the device to open Yaver's overlay, where "Back to Yaver" returns you to the installed app.
206
+ </Text>
207
+ )}
208
+ {/* Polite means closeable. "for now", never "don't show again":
209
+ the badge returns next launch so nobody forgets which build
210
+ they're testing. */}
211
+ <Pressable
212
+ onPress={() => {
213
+ hideYaverModeBadge();
214
+ setOpen(false);
215
+ }}
216
+ style={{ paddingVertical: 10, alignItems: 'center' }}
217
+ >
218
+ <Text style={{ color: '#8a8a96', fontSize: 13 }}>Hide for now</Text>
219
+ </Pressable>
220
+ <Text style={{ color: '#6b6b76', fontSize: 11, textAlign: 'center', lineHeight: 15 }}>
221
+ Hidden until you next launch this build.
222
+ </Text>
223
+
224
+ <Pressable onPress={() => setOpen(false)} style={{ paddingVertical: 10, alignItems: 'center' }}>
225
+ <Text style={{ color: '#8a8a96', fontSize: 13 }}>Close</Text>
226
+ </Pressable>
227
+ </Pressable>
228
+ </Pressable>
229
+ </Modal>
230
+ </>
231
+ );
232
+ }
233
+
234
+ export default YaverModeBadge;
@@ -9,7 +9,7 @@ describe('auth device listing', () => {
9
9
  mockFetch.mockReset();
10
10
  });
11
11
 
12
- it('keeps guest-shared devices in the shared bucket', async () => {
12
+ it('returns the owner devices emitted by /devices/list', async () => {
13
13
  mockFetch.mockResolvedValue({
14
14
  ok: true,
15
15
  json: () =>
@@ -20,24 +20,10 @@ describe('auth device listing', () => {
20
20
  name: 'My Mac',
21
21
  platform: 'macos',
22
22
  isOnline: true,
23
- isGuest: false,
24
23
  quicHost: '10.0.0.10',
25
24
  quicPort: 18080,
26
25
  lastHeartbeat: 123,
27
26
  },
28
- {
29
- deviceId: 'guest-1',
30
- name: 'yaver-test-ephemeral',
31
- platform: 'linux',
32
- isOnline: true,
33
- isGuest: true,
34
- hostName: 'Host User',
35
- hostEmail: 'host@example.com',
36
- accessScope: 'shared-scoped',
37
- quicHost: '198.51.100.20',
38
- quicPort: 18080,
39
- lastHeartbeat: 456,
40
- },
41
27
  ],
42
28
  }),
43
29
  });
@@ -51,43 +37,6 @@ describe('auth device listing', () => {
51
37
  }),
52
38
  );
53
39
  expect(result.owned).toHaveLength(1);
54
- expect(result.shared).toHaveLength(1);
55
40
  expect(result.owned[0].deviceId).toBe('own-1');
56
- expect(result.shared[0]).toMatchObject({
57
- deviceId: 'guest-1',
58
- isGuest: true,
59
- hostEmail: 'host@example.com',
60
- accessScope: 'shared-scoped',
61
- });
62
- });
63
-
64
- it('shows shared devices even when the guest owns no machines', async () => {
65
- mockFetch.mockResolvedValue({
66
- ok: true,
67
- json: () =>
68
- Promise.resolve({
69
- devices: [
70
- {
71
- deviceId: 'guest-only',
72
- name: 'yaver-test-ephemeral',
73
- platform: 'linux',
74
- isOnline: true,
75
- isGuest: true,
76
- hostName: 'Host User',
77
- hostEmail: 'host@example.com',
78
- accessScope: 'shared-scoped',
79
- quicHost: '198.51.100.20',
80
- quicPort: 18080,
81
- lastHeartbeat: 789,
82
- },
83
- ],
84
- }),
85
- });
86
-
87
- const result = await listReachableDevices('guest-only-token');
88
-
89
- expect(result.owned).toEqual([]);
90
- expect(result.shared).toHaveLength(1);
91
- expect(result.shared[0].deviceId).toBe('guest-only');
92
41
  });
93
42
  });
@@ -0,0 +1,129 @@
1
+ // BlackBox relay-password sourcing.
2
+ //
3
+ // This exists because BlackBox.start() used to read the relay password off
4
+ // the feedback config:
5
+ //
6
+ // BlackBox.relayPassword =
7
+ // (feedbackConfig as { relayPassword?: string }).relayPassword ?? '';
8
+ //
9
+ // `relayPassword` is not a key on FeedbackConfig. The cast is what stopped
10
+ // the compiler from saying so, and the field was therefore permanently ''.
11
+ // Since every X-Relay-Password header is written behind an
12
+ // `if (BlackBox.relayPassword)` guard, the header was never sent, and the
13
+ // relay 401s an empty password (relay/server.go). Net effect: on cellular
14
+ // (the only time the relay is used at all) the SSE command channel died,
15
+ // so the reload that lands after an agent fix never fired. On-LAN the
16
+ // password is unused, so this was invisible in local dev — it only ever
17
+ // broke remote users.
18
+ //
19
+ // The contract pinned here: BlackBox takes its relay password from
20
+ // YaverFeedback.getRelayPassword(), the same source P2PClient uses.
21
+
22
+ import { BlackBox } from '../BlackBox';
23
+ import { YaverFeedback } from '../YaverFeedback';
24
+
25
+ jest.mock('react-native', () => ({
26
+ Platform: { OS: 'ios' },
27
+ }));
28
+
29
+ jest.useFakeTimers();
30
+
31
+ const mockFetch = jest.fn();
32
+ global.fetch = mockFetch as any;
33
+
34
+ class MockAbortController {
35
+ signal = { aborted: false };
36
+ abort = jest.fn(() => {
37
+ this.signal.aborted = true;
38
+ });
39
+ }
40
+ global.AbortController = MockAbortController as any;
41
+
42
+ function emptySse(): ReadableStream<Uint8Array> {
43
+ return new ReadableStream<Uint8Array>({
44
+ start(controller) {
45
+ controller.close();
46
+ },
47
+ });
48
+ }
49
+
50
+ beforeEach(() => {
51
+ jest.clearAllMocks();
52
+ mockFetch.mockImplementation((url: string) => {
53
+ if (String(url).includes('/blackbox/command-stream')) {
54
+ return Promise.resolve({ ok: true, body: emptySse() });
55
+ }
56
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
57
+ });
58
+ // A relay-routed agent URL — the shape discovery produces off-LAN.
59
+ YaverFeedback.init({
60
+ agentUrl: 'https://public.yaver.io/d/macmini-1',
61
+ authToken: 'tok',
62
+ });
63
+ });
64
+
65
+ afterEach(() => {
66
+ BlackBox.stop();
67
+ jest.restoreAllMocks();
68
+ });
69
+
70
+ describe('BlackBox relay password', () => {
71
+ it('sends X-Relay-Password on the SSE command stream, sourced from getRelayPassword()', async () => {
72
+ jest.spyOn(YaverFeedback, 'getRelayPassword').mockReturnValue('s3cret-relay-pw');
73
+
74
+ BlackBox.start({ deviceId: 'dev-abc', appName: 'talos-mobile' });
75
+ await Promise.resolve();
76
+ await Promise.resolve();
77
+
78
+ const sseCall = mockFetch.mock.calls.find(([url]) =>
79
+ String(url).includes('/blackbox/command-stream'),
80
+ );
81
+ expect(sseCall).toBeDefined();
82
+ const [, init] = sseCall!;
83
+ expect(init.headers).toEqual(
84
+ expect.objectContaining({ 'X-Relay-Password': 's3cret-relay-pw' }),
85
+ );
86
+ });
87
+
88
+ it('sends X-Relay-Password on the event flush', async () => {
89
+ jest.spyOn(YaverFeedback, 'getRelayPassword').mockReturnValue('s3cret-relay-pw');
90
+
91
+ // maxBufferSize:1 makes push() flush synchronously on the first event,
92
+ // so this doesn't hang on interval semantics.
93
+ BlackBox.start({ deviceId: 'dev-abc', maxBufferSize: 1 });
94
+ BlackBox.log('hello');
95
+ await Promise.resolve();
96
+
97
+ const flush = mockFetch.mock.calls.find(([url]) =>
98
+ String(url).includes('/blackbox/events'),
99
+ );
100
+ expect(flush).toBeDefined();
101
+ const [, init] = flush!;
102
+ expect(init.headers).toEqual(
103
+ expect.objectContaining({ 'X-Relay-Password': 's3cret-relay-pw' }),
104
+ );
105
+ });
106
+
107
+ it('omits the header entirely when there is no relay password (direct LAN)', async () => {
108
+ // An empty header value is itself a 401 at the relay, so "no password"
109
+ // must mean "no header", not "empty header".
110
+ jest.spyOn(YaverFeedback, 'getRelayPassword').mockReturnValue('');
111
+
112
+ BlackBox.start({ deviceId: 'dev-abc' });
113
+ await Promise.resolve();
114
+ await Promise.resolve();
115
+
116
+ const sseCall = mockFetch.mock.calls.find(([url]) =>
117
+ String(url).includes('/blackbox/command-stream'),
118
+ );
119
+ expect(sseCall).toBeDefined();
120
+ const [, init] = sseCall!;
121
+ expect(init.headers).not.toHaveProperty('X-Relay-Password');
122
+ });
123
+
124
+ it('does not read a relayPassword key off the feedback config', () => {
125
+ // The original bug in one assertion: FeedbackConfig has no such key, so
126
+ // anyone reintroducing that read gets '' and silently breaks the relay.
127
+ expect(YaverFeedback.getConfig()).not.toHaveProperty('relayPassword');
128
+ });
129
+ });
@@ -0,0 +1,111 @@
1
+ // BlackBox auto-start config passthrough.
2
+ //
3
+ // The natural way to configure the flight recorder — and the one the README's
4
+ // own snippet shows — is:
5
+ //
6
+ // YaverFeedback.init({ trigger: 'shake' });
7
+ // BlackBox.start({ appName: 'my-app' });
8
+ //
9
+ // That start() early-returns: a zero-config init has no agentUrl yet, because
10
+ // discovery resolves it asynchronously. The SDK's autoStartBlackBox then fires
11
+ // ~500ms later once the agent and token exist — and used to call start() with
12
+ // no arguments, quietly replacing the host's config with defaults and leaving
13
+ // appName as ''. Hosts had no way to tell: the recorder streamed, just
14
+ // anonymously.
15
+ //
16
+ // config.blackBox is now the supported channel, and auto-start honours it.
17
+
18
+ import { BlackBox } from '../BlackBox';
19
+ import { YaverFeedback } from '../YaverFeedback';
20
+
21
+ jest.mock('react-native', () => ({
22
+ Platform: { OS: 'ios' },
23
+ DeviceEventEmitter: { emit: jest.fn(), addListener: jest.fn() },
24
+ NativeModules: {},
25
+ }));
26
+
27
+ jest.mock('../ShakeDetector', () => ({
28
+ ShakeDetector: jest.fn().mockImplementation(() => ({
29
+ start: jest.fn(),
30
+ stop: jest.fn(),
31
+ })),
32
+ }));
33
+
34
+ jest.useFakeTimers();
35
+
36
+ const mockFetch = jest.fn(() =>
37
+ Promise.resolve({ ok: true, json: () => Promise.resolve({}), body: null }),
38
+ );
39
+ global.fetch = mockFetch as any;
40
+
41
+ class MockAbortController {
42
+ signal = { aborted: false };
43
+ abort = jest.fn();
44
+ }
45
+ global.AbortController = MockAbortController as any;
46
+
47
+ beforeEach(() => {
48
+ jest.clearAllMocks();
49
+ YaverFeedback.destroy();
50
+ });
51
+
52
+ afterEach(() => {
53
+ BlackBox.stop();
54
+ YaverFeedback.destroy();
55
+ });
56
+
57
+ describe('autoStartBlackBox config passthrough', () => {
58
+ it('starts BlackBox with the config passed on init', async () => {
59
+ const startSpy = jest.spyOn(BlackBox, 'start').mockImplementation(() => {});
60
+
61
+ YaverFeedback.init({
62
+ trigger: 'shake',
63
+ agentUrl: 'http://localhost:18080',
64
+ authToken: 'tok',
65
+ blackBox: { appName: 'talos-mobile', flushInterval: 3000, maxBufferSize: 25 },
66
+ });
67
+
68
+ await jest.advanceTimersByTimeAsync(600);
69
+
70
+ expect(startSpy).toHaveBeenCalledWith(
71
+ expect.objectContaining({
72
+ appName: 'talos-mobile',
73
+ flushInterval: 3000,
74
+ maxBufferSize: 25,
75
+ }),
76
+ );
77
+ startSpy.mockRestore();
78
+ });
79
+
80
+ it('still auto-starts when no blackBox config is given', async () => {
81
+ const startSpy = jest.spyOn(BlackBox, 'start').mockImplementation(() => {});
82
+
83
+ YaverFeedback.init({
84
+ trigger: 'shake',
85
+ agentUrl: 'http://localhost:18080',
86
+ authToken: 'tok',
87
+ });
88
+
89
+ await jest.advanceTimersByTimeAsync(600);
90
+
91
+ expect(startSpy).toHaveBeenCalled();
92
+ startSpy.mockRestore();
93
+ });
94
+
95
+ it('does not auto-start when autoStartBlackBox is false', async () => {
96
+ const startSpy = jest.spyOn(BlackBox, 'start').mockImplementation(() => {});
97
+
98
+ YaverFeedback.init({
99
+ trigger: 'shake',
100
+ agentUrl: 'http://localhost:18080',
101
+ authToken: 'tok',
102
+ autoStartBlackBox: false,
103
+ blackBox: { appName: 'talos-mobile' },
104
+ });
105
+
106
+ await jest.advanceTimersByTimeAsync(600);
107
+
108
+ expect(startSpy).not.toHaveBeenCalled();
109
+ startSpy.mockRestore();
110
+ });
111
+ });