yaver-feedback-react-native 0.9.0 → 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 (65) 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 +160 -6
  6. package/dist/P2PClient.d.ts +64 -1
  7. package/dist/P2PClient.js +222 -2
  8. package/dist/ShakeDetector.js +2 -0
  9. package/dist/YaverFeedback.d.ts +98 -0
  10. package/dist/YaverFeedback.js +509 -46
  11. package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
  12. package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
  13. package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
  14. package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
  15. package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
  16. package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
  17. package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
  18. package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
  19. package/dist/__tests__/P2PClient.test.js +2 -2
  20. package/dist/__tests__/ReportIdentity.test.d.ts +1 -0
  21. package/dist/__tests__/ReportIdentity.test.js +168 -0
  22. package/dist/__tests__/SDKToken.test.js +1 -1
  23. package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
  24. package/dist/__tests__/ShakeToggle.test.js +121 -0
  25. package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
  26. package/dist/__tests__/pickTargetDevice.test.js +89 -0
  27. package/dist/__tests__/reloadActions.test.d.ts +1 -0
  28. package/dist/__tests__/reloadActions.test.js +129 -0
  29. package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
  30. package/dist/__tests__/reloadActionsParity.test.js +42 -0
  31. package/dist/__tests__/types.test.js +5 -5
  32. package/dist/_core/device.d.ts +19 -9
  33. package/dist/_core/device.js +20 -14
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.js +11 -1
  36. package/dist/reloadActions.d.ts +88 -0
  37. package/dist/reloadActions.js +200 -0
  38. package/dist/storeShots.d.ts +67 -0
  39. package/dist/storeShots.js +137 -0
  40. package/dist/types.d.ts +158 -1
  41. package/package.json +2 -2
  42. package/src/BlackBox.ts +10 -1
  43. package/src/DeployPanel.tsx +74 -0
  44. package/src/Discovery.ts +13 -2
  45. package/src/FeedbackModal.tsx +176 -14
  46. package/src/P2PClient.ts +235 -2
  47. package/src/ShakeDetector.ts +1 -0
  48. package/src/YaverFeedback.ts +498 -46
  49. package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
  50. package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
  51. package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
  52. package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
  53. package/src/__tests__/P2PClient.test.ts +2 -2
  54. package/src/__tests__/ReportIdentity.test.ts +203 -0
  55. package/src/__tests__/SDKToken.test.ts +1 -1
  56. package/src/__tests__/ShakeToggle.test.ts +153 -0
  57. package/src/__tests__/pickTargetDevice.test.ts +101 -0
  58. package/src/__tests__/reloadActions.test.ts +171 -0
  59. package/src/__tests__/reloadActionsParity.test.ts +49 -0
  60. package/src/__tests__/types.test.ts +5 -5
  61. package/src/_core/device.ts +20 -14
  62. package/src/index.ts +21 -0
  63. package/src/reloadActions.ts +273 -0
  64. package/src/storeShots.ts +189 -0
  65. package/src/types.ts +164 -1
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ // Runtime shake toggling + init() idempotency.
3
+ //
4
+ // Host apps put the feedback SDK behind a settings switch, which means init /
5
+ // enable / disable run repeatedly in one process. Two things used to break
6
+ // there:
7
+ //
8
+ // 1. There was no way to turn the shake catcher off short of
9
+ // setEnabled(false) (which also stops the flight recorder and the agent
10
+ // command channel) or a full re-init. `trigger` is only read inside
11
+ // init(). setShakeEnabled() fills that gap.
12
+ //
13
+ // 2. init() registered a BlackBox command handler and threw away the
14
+ // unsubscribe, so each re-init stacked another. Two inits meant two
15
+ // reloads per agent command; destroy() never cleared them either.
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ const YaverFeedback_1 = require("../YaverFeedback");
18
+ const BlackBox_1 = require("../BlackBox");
19
+ const mockShakeStart = jest.fn();
20
+ const mockShakeStop = jest.fn();
21
+ jest.mock('../ShakeDetector', () => ({
22
+ ShakeDetector: jest.fn().mockImplementation(() => ({
23
+ start: mockShakeStart,
24
+ stop: mockShakeStop,
25
+ })),
26
+ }));
27
+ jest.mock('react-native', () => ({
28
+ Platform: { OS: 'ios' },
29
+ DeviceEventEmitter: { emit: jest.fn(), addListener: jest.fn() },
30
+ NativeModules: {},
31
+ }));
32
+ const mockFetch = jest.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({}) }));
33
+ global.fetch = mockFetch;
34
+ beforeEach(() => {
35
+ jest.clearAllMocks();
36
+ YaverFeedback_1.YaverFeedback.destroy();
37
+ });
38
+ afterEach(() => {
39
+ YaverFeedback_1.YaverFeedback.destroy();
40
+ });
41
+ describe('setShakeEnabled', () => {
42
+ it('arms the shake listener on init when trigger is shake', () => {
43
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
44
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(true);
45
+ });
46
+ it('disarms the listener without disabling the SDK', () => {
47
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
48
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(false);
49
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
50
+ expect(mockShakeStop).toHaveBeenCalled();
51
+ // The point of a separate toggle: the rest of the SDK stays up.
52
+ expect(YaverFeedback_1.YaverFeedback.isEnabled()).toBe(true);
53
+ });
54
+ it('re-arms the listener', () => {
55
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
56
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(false);
57
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(true);
58
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(true);
59
+ });
60
+ it('is idempotent — repeated calls do not stack detectors', () => {
61
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
62
+ mockShakeStart.mockClear();
63
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(true);
64
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(true);
65
+ expect(mockShakeStart).not.toHaveBeenCalled();
66
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(true);
67
+ });
68
+ it('persists onto config so a later enable does not resurrect the listener', () => {
69
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
70
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(false);
71
+ YaverFeedback_1.YaverFeedback.setEnabled(false);
72
+ YaverFeedback_1.YaverFeedback.setEnabled(true);
73
+ // The user said no shake. Cycling the master switch must not undo that.
74
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
75
+ expect(YaverFeedback_1.YaverFeedback.getConfig()?.disableShakeGesture).toBe(true);
76
+ });
77
+ it('does not arm the listener for a non-shake trigger', () => {
78
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'manual', agentUrl: 'http://x:18080', authToken: 't' });
79
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
80
+ YaverFeedback_1.YaverFeedback.setShakeEnabled(true);
81
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
82
+ });
83
+ it('no-ops before init rather than throwing', () => {
84
+ expect(() => YaverFeedback_1.YaverFeedback.setShakeEnabled(true)).not.toThrow();
85
+ expect(YaverFeedback_1.YaverFeedback.isShakeEnabled()).toBe(false);
86
+ });
87
+ });
88
+ describe('init() command-handler registration', () => {
89
+ it('does not stack command handlers across re-inits', () => {
90
+ const cfg = { trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' };
91
+ YaverFeedback_1.YaverFeedback.init(cfg);
92
+ YaverFeedback_1.YaverFeedback.init(cfg);
93
+ YaverFeedback_1.YaverFeedback.init(cfg);
94
+ // Three inits, one live handler. Before the fix this array grew by one
95
+ // each time, so a single agent 'reload' fired three reloads.
96
+ expect(BlackBox_1.BlackBox.commandHandlers.length).toBe(1);
97
+ });
98
+ it('fires a reload exactly once per command after repeated inits', () => {
99
+ // The user-visible symptom the count above stands in for.
100
+ const onReload = jest.fn();
101
+ const cfg = { trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't', onReload };
102
+ YaverFeedback_1.YaverFeedback.init(cfg);
103
+ YaverFeedback_1.YaverFeedback.init(cfg);
104
+ for (const h of BlackBox_1.BlackBox.commandHandlers)
105
+ h({ command: 'reload' });
106
+ expect(onReload).toHaveBeenCalledTimes(1);
107
+ });
108
+ it('destroy() unregisters the command handler', () => {
109
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
110
+ const before = BlackBox_1.BlackBox.commandHandlers.length;
111
+ expect(before).toBeGreaterThan(0);
112
+ YaverFeedback_1.YaverFeedback.destroy();
113
+ expect(BlackBox_1.BlackBox.commandHandlers.length).toBe(before - 1);
114
+ });
115
+ it('destroy() then init() leaves exactly one handler', () => {
116
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
117
+ YaverFeedback_1.YaverFeedback.destroy();
118
+ YaverFeedback_1.YaverFeedback.init({ trigger: 'shake', agentUrl: 'http://x:18080', authToken: 't' });
119
+ expect(BlackBox_1.BlackBox.commandHandlers.length).toBe(1);
120
+ });
121
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ // Target-device selection contract.
3
+ //
4
+ // This exists because pickTargetDevice used to silently reroute: when the
5
+ // user's `preferredDeviceId` was absent from the list — or present but
6
+ // carrying an empty `quicHost` — the preference block fell through to the
7
+ // generic "first fresh machine" search. The user picked their Mac mini and
8
+ // the fix landed on their laptop, with no error on either end.
9
+ //
10
+ // The contract now: an explicit preference is honoured by id alone, or the
11
+ // pick fails. Not connecting is a better failure than connecting to the
12
+ // wrong host.
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ const device_1 = require("../_core/device");
15
+ const constants_1 = require("../_core/constants");
16
+ const NOW = 1700000000000;
17
+ function device(over) {
18
+ return {
19
+ name: over.deviceId,
20
+ platform: 'darwin',
21
+ isOnline: true,
22
+ lastHeartbeat: NOW,
23
+ quicHost: '100.64.0.1',
24
+ quicPort: 18080,
25
+ ...over,
26
+ };
27
+ }
28
+ beforeEach(() => {
29
+ jest.spyOn(Date, 'now').mockReturnValue(NOW);
30
+ });
31
+ afterEach(() => {
32
+ jest.restoreAllMocks();
33
+ });
34
+ describe('pickTargetDevice — explicit preference', () => {
35
+ it('returns the preferred device when it is fresh', () => {
36
+ const macmini = device({ deviceId: 'macmini' });
37
+ const laptop = device({ deviceId: 'laptop' });
38
+ expect((0, device_1.pickTargetDevice)([laptop, macmini], 'macmini')).toBe(macmini);
39
+ });
40
+ it('returns the preferred device even when it is STALE, rather than rerouting', () => {
41
+ const macmini = device({
42
+ deviceId: 'macmini',
43
+ lastHeartbeat: NOW - (constants_1.HEARTBEAT_STALE_MS + 1),
44
+ });
45
+ const laptop = device({ deviceId: 'laptop' });
46
+ expect((0, device_1.isDeviceFresh)(macmini)).toBe(false);
47
+ expect((0, device_1.pickTargetDevice)([laptop, macmini], 'macmini')).toBe(macmini);
48
+ });
49
+ it('returns the preferred device even with NO quicHost — relay addresses it by id', () => {
50
+ // This is the off-LAN shape: no LAN address is published, and the relay
51
+ // route is `<relay>/d/<deviceId>`. An empty quicHost is not a reason to
52
+ // reroute; it is the normal remote case.
53
+ const macmini = device({ deviceId: 'macmini', quicHost: '' });
54
+ const laptop = device({ deviceId: 'laptop' });
55
+ expect((0, device_1.pickTargetDevice)([laptop, macmini], 'macmini')).toBe(macmini);
56
+ });
57
+ it('returns null when the preferred id is absent — never falls through to another machine', () => {
58
+ const laptop = device({ deviceId: 'laptop' });
59
+ const desktop = device({ deviceId: 'desktop' });
60
+ expect((0, device_1.pickTargetDevice)([laptop, desktop], 'macmini')).toBeNull();
61
+ });
62
+ it('returns null for an unknown preference even when exactly one healthy machine exists', () => {
63
+ // The tempting "well, there's only one, just use it" case. Still wrong:
64
+ // the user asked for a specific host.
65
+ expect((0, device_1.pickTargetDevice)([device({ deviceId: 'laptop' })], 'macmini')).toBeNull();
66
+ });
67
+ });
68
+ describe('pickTargetDevice — no preference', () => {
69
+ it('prefers a fresh device with a quicHost over a stale one', () => {
70
+ const stale = device({
71
+ deviceId: 'stale',
72
+ lastHeartbeat: NOW - (constants_1.HEARTBEAT_STALE_MS + 1),
73
+ });
74
+ const fresh = device({ deviceId: 'fresh' });
75
+ expect((0, device_1.pickTargetDevice)([stale, fresh])).toBe(fresh);
76
+ });
77
+ it('falls back to an online device with a quicHost when none are fresh', () => {
78
+ const offline = device({ deviceId: 'offline', isOnline: false });
79
+ const online = device({
80
+ deviceId: 'online',
81
+ lastHeartbeat: NOW - (constants_1.HEARTBEAT_STALE_MS + 1),
82
+ });
83
+ expect((0, device_1.pickTargetDevice)([offline, online])).toBe(online);
84
+ });
85
+ it('returns null for an empty list', () => {
86
+ expect((0, device_1.pickTargetDevice)([], 'macmini')).toBeNull();
87
+ expect((0, device_1.pickTargetDevice)([])).toBeNull();
88
+ });
89
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const reloadActions_1 = require("../reloadActions");
4
+ const DEV = { isDevBuild: true, connected: true };
5
+ describe('reloadActions — the production guard', () => {
6
+ // THIS IS THE GUARD. Prove it by breaking it: flip `if (!opts.isDevBuild)`
7
+ // in reloadActions.ts to `if (opts.isDevBuild)` and this test fails while
8
+ // every other test in this file still passes.
9
+ it('returns NOTHING in a production build, even with a healthy dev server', () => {
10
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'vite' }, { isDevBuild: false, connected: true, includeRebuild: true });
11
+ expect(actions).toEqual([]);
12
+ });
13
+ it('returns actions in a dev build', () => {
14
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'vite' }, DEV);
15
+ expect(actions.map((a) => a.id)).toEqual(['hot', 'full']);
16
+ expect(actions.every((a) => a.enabled)).toBe(true);
17
+ });
18
+ });
19
+ describe('reloadFrameworkFamily', () => {
20
+ it.each([
21
+ ['flutter', 'flutter'],
22
+ ['expo', 'react-native'],
23
+ ['react-native', 'react-native'],
24
+ ['vite', 'web'],
25
+ ['nextjs', 'web'],
26
+ ['', 'unknown'],
27
+ ['godot', 'unknown'],
28
+ ])('maps %s → %s', (framework, family) => {
29
+ expect((0, reloadActions_1.reloadFrameworkFamily)(framework)).toBe(family);
30
+ });
31
+ });
32
+ describe('per-stack labels', () => {
33
+ it('calls the full action a Hot Restart on Flutter (stdin R), not a Full Reload', () => {
34
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'flutter' }, DEV);
35
+ expect(actions.find((a) => a.id === 'hot').label).toBe('Hot Reload');
36
+ expect(actions.find((a) => a.id === 'full').label).toBe('Hot Restart');
37
+ expect(actions.find((a) => a.id === 'full').hint).toContain('(R)');
38
+ });
39
+ it('calls it a Full Reload everywhere else', () => {
40
+ for (const framework of ['expo', 'vite', 'nextjs']) {
41
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, framework }, DEV);
42
+ expect(actions.find((a) => a.id === 'full').label).toBe('Full Reload');
43
+ }
44
+ });
45
+ });
46
+ describe('URL / payload construction', () => {
47
+ it('sends fast for hot and full for full, both to /dev/reload', () => {
48
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'flutter' }, DEV);
49
+ expect((0, reloadActions_1.reloadRequest)(actions[0])).toEqual({
50
+ method: 'POST',
51
+ path: reloadActions_1.RELOAD_PATH,
52
+ body: { mode: 'fast' },
53
+ });
54
+ expect((0, reloadActions_1.reloadRequest)(actions[1])).toEqual({
55
+ method: 'POST',
56
+ path: reloadActions_1.RELOAD_PATH,
57
+ body: { mode: 'full' },
58
+ });
59
+ });
60
+ it('routes the RN bundle rebuild to /dev/reload-app', () => {
61
+ const actions = (0, reloadActions_1.reloadActions)({ running: false }, { ...DEV, includeRebuild: true });
62
+ const rebuild = actions.find((a) => a.id === 'rebuild');
63
+ expect((0, reloadActions_1.reloadRequest)(rebuild)).toEqual({
64
+ method: 'POST',
65
+ path: reloadActions_1.RELOAD_APP_PATH,
66
+ body: { mode: 'bundle' },
67
+ });
68
+ });
69
+ });
70
+ describe('a blocked action NAMES the blocker', () => {
71
+ it('names the missing dev server and the command that starts it', () => {
72
+ const actions = (0, reloadActions_1.reloadActions)({ running: false }, { ...DEV, machineLabel: 'primary' });
73
+ expect(actions).toHaveLength(2);
74
+ for (const action of actions) {
75
+ expect(action.enabled).toBe(false);
76
+ expect(action.disabledReason).toContain('primary');
77
+ expect(action.disabledReason).toContain('yaver dev start');
78
+ }
79
+ });
80
+ it('names "still building" rather than pretending nothing is running', () => {
81
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, building: true, framework: 'expo' }, DEV);
82
+ expect(actions[0].disabledReason).toContain('still building');
83
+ });
84
+ it('names the missing machine when disconnected', () => {
85
+ const actions = (0, reloadActions_1.reloadActions)({ running: true, framework: 'expo' }, { isDevBuild: true, connected: false });
86
+ expect(actions[0].disabledReason).toContain('Not connected');
87
+ });
88
+ it('keeps Rebuild Bundle ENABLED with no dev server — that is its whole point', () => {
89
+ const actions = (0, reloadActions_1.reloadActions)({ running: false }, { ...DEV, includeRebuild: true });
90
+ const rebuild = actions.find((a) => a.id === 'rebuild');
91
+ expect(rebuild.enabled).toBe(true);
92
+ expect(rebuild.disabledReason).toBeUndefined();
93
+ });
94
+ it('disables Rebuild Bundle when there is no machine', () => {
95
+ const actions = (0, reloadActions_1.reloadActions)({ running: true }, { isDevBuild: true, connected: false, includeRebuild: true });
96
+ expect(actions.find((a) => a.id === 'rebuild').enabled).toBe(false);
97
+ });
98
+ });
99
+ describe('describeReloadFailure names a cause, never "failed"', () => {
100
+ it('503 → no dev server', () => {
101
+ expect((0, reloadActions_1.describeReloadFailure)(503, 'dev server not available')).toContain('No dev server is running');
102
+ });
103
+ it('framework cannot hot reload → says so and points at the alternative', () => {
104
+ const msg = (0, reloadActions_1.describeReloadFailure)(500, 'unity does not support hot reload', {
105
+ running: true,
106
+ framework: 'unity',
107
+ });
108
+ expect(msg).toContain('unity');
109
+ expect(msg).toContain('Rebuild Bundle');
110
+ });
111
+ it('loopback connection refused → the dev server is not listening', () => {
112
+ const msg = (0, reloadActions_1.describeReloadFailure)(502, 'Get "http://127.0.0.1:8081/reload": dial tcp 127.0.0.1:8081: connect: connection refused');
113
+ expect(msg).toContain('not listening');
114
+ expect(msg).toContain('yaver dev start');
115
+ });
116
+ it('401/403 → session, not server', () => {
117
+ expect((0, reloadActions_1.describeReloadFailure)(401, '')).toContain('sign in again');
118
+ expect((0, reloadActions_1.describeReloadFailure)(403, '')).toContain('sign in again');
119
+ });
120
+ it('404 → the agent is too old, and says how to update it', () => {
121
+ expect((0, reloadActions_1.describeReloadFailure)(404, 'not found')).toContain('yaver-cli@latest');
122
+ });
123
+ it('5xx → points at the agent log', () => {
124
+ expect((0, reloadActions_1.describeReloadFailure)(500, 'boom')).toContain('yaver logs');
125
+ });
126
+ it('status 0 (transport never answered) → machine reachability', () => {
127
+ expect((0, reloadActions_1.describeReloadFailure)(0, '')).toContain('yaver serve');
128
+ });
129
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fs_1 = require("fs");
4
+ const path_1 = require("path");
5
+ /**
6
+ * `reloadActions.ts` is duplicated into yaver-feedback-react-native and
7
+ * yaver-feedback-web on purpose — they are two independently published npm
8
+ * packages, and neither may depend on the other.
9
+ *
10
+ * Duplication without a guard is DRIFT, and drift in this file is the worst
11
+ * kind: it is a *policy* file. If the web copy stops returning [] for a
12
+ * production build and the RN copy still does, the two SDKs disagree about
13
+ * whether a shipped app may show a reload button — and nothing in `tsc`
14
+ * notices, because they are separate compilation units.
15
+ *
16
+ * This is the same shape as beaconParity.test.ts in mobile/: read both
17
+ * sources, assert they agree. Prove it by breaking it — change one word in
18
+ * either copy and this test fails.
19
+ */
20
+ const RN_COPY = (0, path_1.join)(__dirname, '..', 'reloadActions.ts');
21
+ const WEB_COPY = (0, path_1.join)(__dirname, '..', '..', '..', // sdk/feedback/react-native
22
+ 'web', 'src', 'reloadActions.ts');
23
+ describe('reloadActions parity between the RN and web SDKs', () => {
24
+ it('both copies exist', () => {
25
+ expect(() => (0, fs_1.readFileSync)(RN_COPY, 'utf8')).not.toThrow();
26
+ expect(() => (0, fs_1.readFileSync)(WEB_COPY, 'utf8')).not.toThrow();
27
+ });
28
+ it('is byte-identical across the two packages', () => {
29
+ const rn = (0, fs_1.readFileSync)(RN_COPY, 'utf8');
30
+ const web = (0, fs_1.readFileSync)(WEB_COPY, 'utf8');
31
+ expect(rn).toEqual(web);
32
+ });
33
+ it('still carries the production guard in BOTH copies', () => {
34
+ // Named separately from the byte comparison so that if someone
35
+ // legitimately reformats both files the failure still points at the one
36
+ // line that actually matters.
37
+ for (const [name, path] of [['react-native', RN_COPY], ['web', WEB_COPY]]) {
38
+ const source = (0, fs_1.readFileSync)(path, 'utf8');
39
+ expect(`${name}: ${source.includes('if (!opts.isDevBuild) return [];')}`).toEqual(`${name}: true`);
40
+ }
41
+ });
42
+ });
@@ -39,7 +39,7 @@ describe('React Native SDK types', () => {
39
39
  const bundle = {
40
40
  metadata: {
41
41
  timestamp: '2026-03-24T12:00:00Z',
42
- device: {
42
+ deviceInfo: {
43
43
  platform: 'ios',
44
44
  osVersion: '18.0',
45
45
  model: 'iPhone 16 Pro',
@@ -55,7 +55,7 @@ describe('React Native SDK types', () => {
55
55
  screenshots: [],
56
56
  };
57
57
  expect(bundle.metadata.timestamp).toBe('2026-03-24T12:00:00Z');
58
- expect(bundle.metadata.device.platform).toBe('ios');
58
+ expect(bundle.metadata.deviceInfo.platform).toBe('ios');
59
59
  expect(bundle.screenshots).toEqual([]);
60
60
  expect(bundle.video).toBeUndefined();
61
61
  });
@@ -63,7 +63,7 @@ describe('React Native SDK types', () => {
63
63
  const bundle = {
64
64
  metadata: {
65
65
  timestamp: '2026-03-24T12:00:00Z',
66
- device: {
66
+ deviceInfo: {
67
67
  platform: 'android',
68
68
  osVersion: '15',
69
69
  model: 'Pixel 9',
@@ -146,7 +146,7 @@ describe('React Native SDK types', () => {
146
146
  bundle: {
147
147
  metadata: {
148
148
  timestamp: 'now',
149
- device: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
149
+ deviceInfo: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
150
150
  app: {},
151
151
  },
152
152
  screenshots: [],
@@ -164,7 +164,7 @@ describe('React Native SDK types', () => {
164
164
  bundle: {
165
165
  metadata: {
166
166
  timestamp: 'now',
167
- device: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
167
+ deviceInfo: { platform: 'ios', osVersion: '18', model: 'iPhone', screenWidth: 393, screenHeight: 852 },
168
168
  app: {},
169
169
  },
170
170
  screenshots: [],
@@ -39,18 +39,28 @@ export declare function mergeDeviceEntries(a: CoreDevice, b: CoreDevice): CoreDe
39
39
  */
40
40
  export declare function collapseDevices(devices: CoreDevice[]): CoreDevice[];
41
41
  /**
42
- * "Fresh" matches the mobile app: online + heartbeat < 90 s. Clients
43
- * read Convex's `isOnline` first (backend already applies its own 90 s
44
- * gate from the server clock), then use this helper when they need the
45
- * phone-side freshness opinion too — e.g. for auto-connect picks.
42
+ * "Fresh" matches the mobile app: online + heartbeat within
43
+ * HEARTBEAT_STALE_MS. Clients read Convex's `isOnline` first (the backend
44
+ * applies the same gate from the server clock), then use this helper when
45
+ * they need the phone-side freshness opinion too — e.g. for auto-connect
46
+ * picks.
46
47
  */
47
48
  export declare function isDeviceFresh(d: CoreDevice, now?: number): boolean;
48
49
  /**
49
- * Choose the best candidate for an auto-connect attempt. Preference:
50
- * 1. explicit `preferredDeviceId` that's still fresh
51
- * 2. fresh (online + recent heartbeat) + has a quicHost
52
- * 3. online + has a quicHost
53
- * 4. first with a quicHost
50
+ * Choose the best candidate for an auto-connect attempt.
51
+ *
52
+ * An explicit `preferredDeviceId` is honoured by id ALONE, or not at all:
53
+ * - A missing `quicHost` is not grounds to reroute. Relay transport
54
+ * addresses a device by id (`<relay>/d/<deviceId>`), so the entry is
55
+ * still reachable off-LAN — which is precisely when quicHost is absent.
56
+ * - If the id is not in the list, return null. Falling through to another
57
+ * machine silently lands the user's fix on the wrong host: they pick the
58
+ * Mac mini, the commit shows up on the laptop, and nothing reports an
59
+ * error. Not connecting is the better failure — the caller surfaces
60
+ * "selected machine missing, re-select it".
61
+ *
62
+ * With no preference, fall back: fresh + quicHost → online + quicHost →
63
+ * first with a quicHost.
54
64
  */
55
65
  export declare function pickTargetDevice(devices: CoreDevice[], preferredDeviceId?: string): CoreDevice | null;
56
66
  /**
@@ -182,10 +182,11 @@ function collapseDevices(devices) {
182
182
  }
183
183
  // ── Freshness + target pick ───────────────────────────────────────────
184
184
  /**
185
- * "Fresh" matches the mobile app: online + heartbeat < 90 s. Clients
186
- * read Convex's `isOnline` first (backend already applies its own 90 s
187
- * gate from the server clock), then use this helper when they need the
188
- * phone-side freshness opinion too — e.g. for auto-connect picks.
185
+ * "Fresh" matches the mobile app: online + heartbeat within
186
+ * HEARTBEAT_STALE_MS. Clients read Convex's `isOnline` first (the backend
187
+ * applies the same gate from the server clock), then use this helper when
188
+ * they need the phone-side freshness opinion too — e.g. for auto-connect
189
+ * picks.
189
190
  */
190
191
  function isDeviceFresh(d, now = Date.now()) {
191
192
  if (!d.isOnline)
@@ -195,21 +196,26 @@ function isDeviceFresh(d, now = Date.now()) {
195
196
  return now - d.lastHeartbeat < constants_1.HEARTBEAT_STALE_MS;
196
197
  }
197
198
  /**
198
- * Choose the best candidate for an auto-connect attempt. Preference:
199
- * 1. explicit `preferredDeviceId` that's still fresh
200
- * 2. fresh (online + recent heartbeat) + has a quicHost
201
- * 3. online + has a quicHost
202
- * 4. first with a quicHost
199
+ * Choose the best candidate for an auto-connect attempt.
200
+ *
201
+ * An explicit `preferredDeviceId` is honoured by id ALONE, or not at all:
202
+ * - A missing `quicHost` is not grounds to reroute. Relay transport
203
+ * addresses a device by id (`<relay>/d/<deviceId>`), so the entry is
204
+ * still reachable off-LAN — which is precisely when quicHost is absent.
205
+ * - If the id is not in the list, return null. Falling through to another
206
+ * machine silently lands the user's fix on the wrong host: they pick the
207
+ * Mac mini, the commit shows up on the laptop, and nothing reports an
208
+ * error. Not connecting is the better failure — the caller surfaces
209
+ * "selected machine missing, re-select it".
210
+ *
211
+ * With no preference, fall back: fresh + quicHost → online + quicHost →
212
+ * first with a quicHost.
203
213
  */
204
214
  function pickTargetDevice(devices, preferredDeviceId) {
205
215
  if (!devices.length)
206
216
  return null;
207
217
  if (preferredDeviceId) {
208
- const preferred = devices.find((d) => d.deviceId === preferredDeviceId && d.quicHost);
209
- if (preferred && isDeviceFresh(preferred))
210
- return preferred;
211
- if (preferred)
212
- return preferred;
218
+ return devices.find((d) => d.deviceId === preferredDeviceId) ?? null;
213
219
  }
214
220
  const fresh = devices.find((d) => isDeviceFresh(d) && d.quicHost);
215
221
  if (fresh)
package/dist/index.d.ts CHANGED
@@ -28,12 +28,16 @@
28
28
  * ```
29
29
  */
30
30
  export { YaverFeedback } from './YaverFeedback';
31
+ export { captureStoreScreenshots } from './storeShots';
32
+ export type { CaptureStoreScreenshotsOptions, CaptureStoreScreenshotsResult, StoreShotFrame, } from './storeShots';
31
33
  export { BlackBox } from './BlackBox';
32
34
  export { YaverUpdates } from './YaverUpdates';
33
35
  export type { YaverUpdatesConfig, PendingUpdate } from './YaverUpdates';
34
36
  export { initExpo } from './expo';
35
37
  export { YaverDiscovery } from './Discovery';
36
38
  export { P2PClient } from './P2PClient';
39
+ export { reloadActions, reloadRequest, reloadFrameworkFamily, describeReloadFailure, RELOAD_PATH, RELOAD_APP_PATH, } from './reloadActions';
40
+ export type { ReloadAction, ReloadActionId, ReloadActionsOptions, ReloadWireMode, DevServerSnapshot, } from './reloadActions';
37
41
  export { YaverConnectionScreen } from './ConnectionScreen';
38
42
  export { YaverLoginScreen } from './LoginScreen';
39
43
  export type { YaverLoginScreenProps } from './LoginScreen';
package/dist/index.js CHANGED
@@ -29,9 +29,12 @@
29
29
  * ```
30
30
  */
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.uploadFeedback = exports.isVideoRecording = exports.stopVideoRecording = exports.startVideoRecording = exports.pickFeedbackFile = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.acceptGuestInvitation = exports.acceptGuestByCode = exports.findInviteByCode = exports.fetchGuestHosts = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = exports.getToken = exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.clearQuickIconDisabled = exports.setQuickIconDisabled = exports.getQuickIconDisabled = exports.FixReport = exports.QuickActionIcon = exports.FeedbackModal = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.PairDeviceModal = exports.YaverGuestOnboardingScreen = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.YaverFeedback = void 0;
32
+ exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.acceptGuestInvitation = exports.acceptGuestByCode = exports.findInviteByCode = exports.fetchGuestHosts = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = exports.getToken = exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.clearQuickIconDisabled = exports.setQuickIconDisabled = exports.getQuickIconDisabled = exports.FixReport = exports.QuickActionIcon = exports.FeedbackModal = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.PairDeviceModal = exports.YaverGuestOnboardingScreen = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.RELOAD_APP_PATH = exports.RELOAD_PATH = exports.describeReloadFailure = exports.reloadFrameworkFamily = exports.reloadRequest = exports.reloadActions = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.captureStoreScreenshots = exports.YaverFeedback = void 0;
33
+ exports.uploadFeedback = exports.isVideoRecording = exports.stopVideoRecording = exports.startVideoRecording = exports.pickFeedbackFile = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = void 0;
33
34
  var YaverFeedback_1 = require("./YaverFeedback");
34
35
  Object.defineProperty(exports, "YaverFeedback", { enumerable: true, get: function () { return YaverFeedback_1.YaverFeedback; } });
36
+ var storeShots_1 = require("./storeShots");
37
+ Object.defineProperty(exports, "captureStoreScreenshots", { enumerable: true, get: function () { return storeShots_1.captureStoreScreenshots; } });
35
38
  var BlackBox_1 = require("./BlackBox");
36
39
  Object.defineProperty(exports, "BlackBox", { enumerable: true, get: function () { return BlackBox_1.BlackBox; } });
37
40
  var YaverUpdates_1 = require("./YaverUpdates");
@@ -42,6 +45,13 @@ var Discovery_1 = require("./Discovery");
42
45
  Object.defineProperty(exports, "YaverDiscovery", { enumerable: true, get: function () { return Discovery_1.YaverDiscovery; } });
43
46
  var P2PClient_1 = require("./P2PClient");
44
47
  Object.defineProperty(exports, "P2PClient", { enumerable: true, get: function () { return P2PClient_1.P2PClient; } });
48
+ var reloadActions_1 = require("./reloadActions");
49
+ Object.defineProperty(exports, "reloadActions", { enumerable: true, get: function () { return reloadActions_1.reloadActions; } });
50
+ Object.defineProperty(exports, "reloadRequest", { enumerable: true, get: function () { return reloadActions_1.reloadRequest; } });
51
+ Object.defineProperty(exports, "reloadFrameworkFamily", { enumerable: true, get: function () { return reloadActions_1.reloadFrameworkFamily; } });
52
+ Object.defineProperty(exports, "describeReloadFailure", { enumerable: true, get: function () { return reloadActions_1.describeReloadFailure; } });
53
+ Object.defineProperty(exports, "RELOAD_PATH", { enumerable: true, get: function () { return reloadActions_1.RELOAD_PATH; } });
54
+ Object.defineProperty(exports, "RELOAD_APP_PATH", { enumerable: true, get: function () { return reloadActions_1.RELOAD_APP_PATH; } });
45
55
  var ConnectionScreen_1 = require("./ConnectionScreen");
46
56
  Object.defineProperty(exports, "YaverConnectionScreen", { enumerable: true, get: function () { return ConnectionScreen_1.YaverConnectionScreen; } });
47
57
  var LoginScreen_1 = require("./LoginScreen");