yaver-feedback-react-native 0.9.2 → 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 (103) hide show
  1. package/README.md +102 -1
  2. package/dist/AuthOverlay.d.ts +1 -14
  3. package/dist/AuthOverlay.js +9 -62
  4. package/dist/Discovery.js +0 -6
  5. package/dist/DogfoodRuntime.d.ts +124 -0
  6. package/dist/DogfoodRuntime.js +273 -0
  7. package/dist/FeedbackModal.js +347 -61
  8. package/dist/LoginScreen.d.ts +1 -5
  9. package/dist/LoginScreen.js +2 -6
  10. package/dist/MachinePickerScreen.d.ts +1 -3
  11. package/dist/MachinePickerScreen.js +6 -18
  12. package/dist/P2PClient.d.ts +116 -3
  13. package/dist/P2PClient.js +273 -3
  14. package/dist/P2PDogfoodDriver.d.ts +12 -0
  15. package/dist/P2PDogfoodDriver.js +118 -0
  16. package/dist/PairDeviceModal.d.ts +2 -3
  17. package/dist/VibeChatScreen.d.ts +11 -1
  18. package/dist/VibeChatScreen.js +200 -41
  19. package/dist/YaverFeedback.d.ts +34 -0
  20. package/dist/YaverFeedback.js +124 -19
  21. package/dist/YaverModeBadge.d.ts +22 -0
  22. package/dist/YaverModeBadge.js +219 -0
  23. package/dist/__tests__/AuthDevices.test.js +1 -48
  24. package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
  25. package/dist/__tests__/DogfoodRuntime.test.js +116 -0
  26. package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
  27. package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
  28. package/dist/__tests__/ReportIdentity.test.d.ts +25 -1
  29. package/dist/__tests__/ReportIdentity.test.js +34 -22
  30. package/dist/__tests__/YaverFeedback.test.js +13 -3
  31. package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
  32. package/dist/__tests__/deviceDogfood.test.js +86 -0
  33. package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
  34. package/dist/__tests__/dogfoodPolicy.test.js +33 -0
  35. package/dist/_core/ansi.d.ts +117 -0
  36. package/dist/_core/ansi.js +468 -0
  37. package/dist/_core/ansi.test.d.ts +1 -0
  38. package/dist/_core/ansi.test.js +225 -0
  39. package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
  40. package/dist/_core/buildFeedbackPrompt.js +18 -72
  41. package/dist/_core/constants.d.ts +19 -6
  42. package/dist/_core/constants.js +20 -7
  43. package/dist/_core/device.d.ts +9 -23
  44. package/dist/_core/device.js +13 -28
  45. package/dist/_core/endpoints.d.ts +0 -7
  46. package/dist/_core/endpoints.js +0 -7
  47. package/dist/_core/index.d.ts +4 -0
  48. package/dist/_core/index.js +4 -0
  49. package/dist/_core/remoteless.d.ts +44 -0
  50. package/dist/_core/remoteless.js +75 -0
  51. package/dist/_core/trace.d.ts +47 -0
  52. package/dist/_core/trace.js +38 -0
  53. package/dist/_core/trace.test.d.ts +1 -0
  54. package/dist/_core/trace.test.js +60 -0
  55. package/dist/auth.d.ts +3 -60
  56. package/dist/auth.js +6 -88
  57. package/dist/deviceDogfood.d.ts +53 -0
  58. package/dist/deviceDogfood.js +137 -0
  59. package/dist/dogfoodPolicy.d.ts +25 -0
  60. package/dist/dogfoodPolicy.js +24 -0
  61. package/dist/index.d.ts +13 -4
  62. package/dist/index.js +24 -8
  63. package/dist/reloadActions.js +2 -2
  64. package/dist/types.d.ts +50 -7
  65. package/package.json +12 -3
  66. package/src/AuthOverlay.tsx +20 -106
  67. package/src/Discovery.ts +0 -6
  68. package/src/DogfoodRuntime.ts +373 -0
  69. package/src/FeedbackModal.tsx +445 -67
  70. package/src/LoginScreen.tsx +2 -22
  71. package/src/MachinePickerScreen.tsx +6 -21
  72. package/src/P2PClient.ts +347 -4
  73. package/src/P2PDogfoodDriver.ts +132 -0
  74. package/src/PairDeviceModal.tsx +2 -3
  75. package/src/VibeChatScreen.tsx +232 -42
  76. package/src/YaverFeedback.ts +133 -22
  77. package/src/YaverModeBadge.tsx +234 -0
  78. package/src/__tests__/AuthDevices.test.ts +1 -52
  79. package/src/__tests__/DogfoodRuntime.test.ts +135 -0
  80. package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
  81. package/src/__tests__/ReportIdentity.test.ts +36 -27
  82. package/src/__tests__/YaverFeedback.test.ts +15 -3
  83. package/src/__tests__/deviceDogfood.test.ts +77 -0
  84. package/src/__tests__/dogfoodPolicy.test.ts +34 -0
  85. package/src/_core/ansi.test.ts +250 -0
  86. package/src/_core/ansi.ts +475 -0
  87. package/src/_core/buildFeedbackPrompt.ts +18 -95
  88. package/src/_core/constants.ts +20 -6
  89. package/src/_core/device.ts +13 -30
  90. package/src/_core/endpoints.ts +0 -7
  91. package/src/_core/index.ts +4 -0
  92. package/src/_core/remoteless.ts +110 -0
  93. package/src/_core/trace.test.ts +58 -0
  94. package/src/_core/trace.ts +75 -0
  95. package/src/auth.ts +7 -156
  96. package/src/deviceDogfood.ts +171 -0
  97. package/src/dogfoodPolicy.ts +40 -0
  98. package/src/index.ts +40 -11
  99. package/src/reloadActions.ts +2 -2
  100. package/src/types.ts +45 -7
  101. package/dist/GuestOnboardingScreen.d.ts +0 -8
  102. package/dist/GuestOnboardingScreen.js +0 -282
  103. package/src/GuestOnboardingScreen.tsx +0 -307
@@ -15,7 +15,6 @@ import {
15
15
  getToken,
16
16
  getSelectedDeviceId,
17
17
  listReachableDevices,
18
- mintGuestSdkToken,
19
18
  clearToken,
20
19
  clearSelectedDeviceId,
21
20
  DEFAULT_CONVEX_SITE_URL,
@@ -27,6 +26,15 @@ import {
27
26
  setQuickIconColorPreset,
28
27
  QuickIconColorPreset,
29
28
  } from './preferences';
29
+ import { resolveSDKDogfood, type SDKDogfoodStatus } from './dogfoodPolicy';
30
+ import { YaverDeviceDogfood, type DeviceDogfoodOptions, type DeviceDogfoodSession, type DeviceDogfoodState } from './deviceDogfood';
31
+
32
+ export interface DogfoodOnboardingOptions extends DeviceDogfoodOptions {
33
+ /** Hint used to preselect the matching project returned by the owner machine. */
34
+ projectName?: string;
35
+ /** Framework fallback when the machine has not classified the project yet. */
36
+ framework?: string;
37
+ }
30
38
 
31
39
  function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
32
40
  const maybeNodeTimer = timer as unknown as { unref?: () => void };
@@ -158,6 +166,7 @@ if (IS_HOST_MODE) {
158
166
  let config: FeedbackConfig | null = null;
159
167
  let enabled = false;
160
168
  let p2pClient: P2PClient | null = null;
169
+ let renderP2PClient: P2PClient | null = null;
161
170
  let shakeDetector: ShakeDetector | null = null;
162
171
  /** Unsubscribe for the BlackBox command handler registered by init(). Held so
163
172
  * a re-init (or destroy) can drop the old one instead of stacking. */
@@ -169,6 +178,7 @@ let p2pRelayPassword: string = '';
169
178
  let autoStartTimer: ReturnType<typeof setTimeout> | null = null;
170
179
  let reportLaunchInFlight = false;
171
180
  let crashReportInFlight = false;
181
+ let dogfoodOnboarding: DogfoodOnboardingOptions | null = null;
172
182
 
173
183
  /** Resolve the user's relay password by validating their auth token
174
184
  * against Convex. Used whenever we (re)build the P2PClient so a
@@ -241,42 +251,35 @@ const flagCache: Map<string, { value: unknown; at: number }> = new Map();
241
251
  */
242
252
  export class YaverFeedback {
243
253
  private static async resolveP2PAuthToken(): Promise<string | null> {
244
- if (!config?.authToken) return null;
245
- if (!config.preferredDeviceId) return config.authToken;
246
- const devices = await listReachableDevices(config.authToken);
247
- const all = [...devices.owned, ...devices.shared];
248
- const selected = all.find((device) => device.deviceId === config?.preferredDeviceId);
249
- if (!selected || !selected.isGuest || selected.accessScope !== 'shared-scoped') {
250
- return config.authToken;
251
- }
252
- if (!selected.hostUserId) {
253
- return config.authToken;
254
- }
255
- const delegated = await mintGuestSdkToken(
256
- config.authToken,
257
- selected.hostUserId,
258
- selected.deviceId,
259
- );
260
- return delegated.token;
254
+ return config?.authToken ?? null;
261
255
  }
262
256
 
263
257
  private static async rebuildP2PClient(agentUrl?: string): Promise<void> {
264
- if (!config) return;
265
- const effectiveUrl = agentUrl ?? config.agentUrl;
258
+ const currentConfig = config;
259
+ if (!currentConfig) return;
260
+ const effectiveUrl = agentUrl ?? currentConfig.agentUrl;
266
261
  if (!effectiveUrl) {
267
262
  p2pClient = null;
263
+ renderP2PClient = null;
268
264
  p2pAuthToken = null;
269
265
  return;
270
266
  }
271
267
  const token = await YaverFeedback.resolveP2PAuthToken();
268
+ // reset() can clear config while relay-password resolution is awaiting.
269
+ // Do not resurrect a client from that stale init generation.
270
+ if (config !== currentConfig) return;
272
271
  if (!token) {
273
272
  p2pClient = null;
273
+ renderP2PClient = null;
274
274
  p2pAuthToken = null;
275
275
  return;
276
276
  }
277
277
  p2pAuthToken = token;
278
278
  const rp = await resolveRelayPassword(token);
279
+ if (config !== currentConfig) return;
279
280
  p2pClient = new P2PClient(effectiveUrl, token, rp);
281
+ const renderUrl = currentConfig.renderAgentUrl || effectiveUrl;
282
+ renderP2PClient = renderUrl === effectiveUrl ? p2pClient : new P2PClient(renderUrl, token, rp);
280
283
  }
281
284
 
282
285
  /**
@@ -365,6 +368,9 @@ export class YaverFeedback {
365
368
  maxRecordingDuration: 120,
366
369
  autoLogin: true,
367
370
  ...cfg,
371
+ agentUrl: cfg.codingAgentUrl || cfg.agentUrl,
372
+ codingDeviceId: cfg.codingDeviceId || cfg.preferredDeviceId,
373
+ renderDeviceId: cfg.renderDeviceId || cfg.codingDeviceId || cfg.preferredDeviceId,
368
374
  };
369
375
  if (IS_HOST_MODE) {
370
376
  // sfmg / talos / etc. running inside Yaver mobile (compile-time
@@ -441,11 +447,16 @@ export class YaverFeedback {
441
447
  // is set, so set a placeholder header here that won't 401 a
442
448
  // direct-LAN url and will be overwritten before any relay hop.
443
449
  p2pClient = new P2PClient(config.agentUrl, config.authToken ?? '', p2pRelayPassword);
450
+ const renderUrl = config.renderAgentUrl || config.agentUrl;
451
+ renderP2PClient = renderUrl === config.agentUrl
452
+ ? p2pClient
453
+ : new P2PClient(renderUrl, config.authToken ?? '', p2pRelayPassword);
444
454
  if (config.authToken) {
445
455
  void YaverFeedback.rebuildP2PClient(config.agentUrl);
446
456
  }
447
457
  } else {
448
458
  p2pClient = null;
459
+ renderP2PClient = null;
449
460
  // Auto-discover agent in the background when convexUrl or LAN is available
450
461
  if (enabled && (config.authToken || config.preferredDeviceId)) {
451
462
  YaverFeedback.discoverAgent();
@@ -796,6 +807,27 @@ export class YaverFeedback {
796
807
  DeviceEventEmitter.emit('yaverFeedback:startMachinePicker');
797
808
  }
798
809
 
810
+ /** Begin the reusable host-app Dogfood wizard. Owner OAuth and machine
811
+ * selection intentionally happen before installation enrollment/runtime
812
+ * controls because starting builds or runners is owner-level authority. */
813
+ static beginDogfoodOnboarding(options: DogfoodOnboardingOptions): void {
814
+ dogfoodOnboarding = options;
815
+ if (!config) YaverFeedback.init({ autoLogin: true } as FeedbackConfig);
816
+ if (!YaverFeedback.isAuthed()) {
817
+ YaverFeedback.showLogin();
818
+ return;
819
+ }
820
+ YaverFeedback.showMachinePicker();
821
+ }
822
+
823
+ static getDogfoodOnboarding(): DogfoodOnboardingOptions | null {
824
+ return dogfoodOnboarding;
825
+ }
826
+
827
+ static clearDogfoodOnboarding(): void {
828
+ dogfoodOnboarding = null;
829
+ }
830
+
799
831
  /**
800
832
  * Update the selected remote device. Resets the cached agent URL so the
801
833
  * next `startReport()` (or FloatingButton press) rediscovers against the
@@ -803,9 +835,17 @@ export class YaverFeedback {
803
835
  */
804
836
  static async setPreferredDevice(deviceId: string): Promise<void> {
805
837
  if (!config) return;
838
+ const wasUnified = !config.renderDeviceId
839
+ || config.renderDeviceId === config.codingDeviceId
840
+ || config.renderDeviceId === config.preferredDeviceId;
806
841
  config.preferredDeviceId = deviceId;
842
+ config.codingDeviceId = deviceId;
843
+ if (wasUnified) config.renderDeviceId = deviceId;
807
844
  config.agentUrl = undefined;
845
+ config.codingAgentUrl = undefined;
846
+ if (wasUnified) config.renderAgentUrl = undefined;
808
847
  p2pClient = null;
848
+ renderP2PClient = null;
809
849
  p2pAuthToken = null;
810
850
  await YaverFeedback.discoverAgent();
811
851
  }
@@ -815,8 +855,7 @@ export class YaverFeedback {
815
855
  if (!config?.authToken || !config.preferredDeviceId) return null;
816
856
  const preferredDeviceId = config.preferredDeviceId;
817
857
  const devices = await listReachableDevices(config.authToken);
818
- const all = [...devices.owned, ...devices.shared];
819
- return all.find((device) => device.deviceId === preferredDeviceId) ?? null;
858
+ return devices.owned.find((device) => device.deviceId === preferredDeviceId) ?? null;
820
859
  }
821
860
 
822
861
  /**
@@ -876,6 +915,7 @@ export class YaverFeedback {
876
915
  config.agentUrl = undefined;
877
916
  }
878
917
  p2pClient = null;
918
+ renderP2PClient = null;
879
919
  p2pAuthToken = null;
880
920
  }
881
921
 
@@ -1133,6 +1173,63 @@ export class YaverFeedback {
1133
1173
  return config;
1134
1174
  }
1135
1175
 
1176
+ /** Current third-party SDK mode. Fails closed unless enabled + account match. */
1177
+ static getDogfoodStatus(): SDKDogfoodStatus {
1178
+ return resolveSDKDogfood(config?.dogfood);
1179
+ }
1180
+
1181
+ /** One-call, account-free Dogfood bootstrap for third-party apps. On first
1182
+ * launch it creates/proves the installation key and returns pending; after
1183
+ * owner approval the same call obtains a short-lived scoped Yaver session
1184
+ * and enables Dogfood UX without the host app implementing OAuth. */
1185
+ static async enableDeviceDogfood(options: DeviceDogfoodOptions): Promise<{
1186
+ status: DeviceDogfoodState;
1187
+ installationId: string;
1188
+ session: DeviceDogfoodSession | null;
1189
+ }> {
1190
+ const client = new YaverDeviceDogfood(options);
1191
+ let status = await client.status();
1192
+ if (status === 'unregistered' || status === 'pending' || status === 'cancelled' || status === 'revoked' || status === 'superseded') {
1193
+ const enrolled = status === 'unregistered' || status === 'pending'
1194
+ ? await client.enroll(Platform.OS)
1195
+ : await client.reRegister(Platform.OS);
1196
+ status = enrolled.status;
1197
+ }
1198
+ const info = await client.enrollmentInfo();
1199
+ const session = status === 'active' ? await client.session() : null;
1200
+ if (!config) YaverFeedback.init({ autoLogin: false } as FeedbackConfig);
1201
+ if (config) {
1202
+ config.dogfood = {
1203
+ enabled: status === 'active' && !!session,
1204
+ appId: options.appId,
1205
+ installationId: info.installationId,
1206
+ installationStatus: status === 'active' && session ? 'active' : status === 'unregistered' ? 'pending' : status,
1207
+ label: options.label,
1208
+ };
1209
+ // A normal Yaver OAuth session remains authoritative for the runtime
1210
+ // wizard. Only account-free hosts adopt the narrow installation token.
1211
+ if (session && !config.authToken) await YaverFeedback.setAuthToken(session.token);
1212
+ }
1213
+ try {
1214
+ const { DeviceEventEmitter } = require('react-native');
1215
+ DeviceEventEmitter.emit('yaverFeedback:dogfoodChanged', { active: !!session, status });
1216
+ } catch { /* noop */ }
1217
+ return { status, installationId: info.installationId, session };
1218
+ }
1219
+
1220
+ /** Confirmed by the Y badge/UI before this is called. Reverts this SDK to
1221
+ * normal Feedback mode for the remainder of the app run. */
1222
+ static async exitDogfoodMode(): Promise<void> {
1223
+ const cfg = config?.dogfood;
1224
+ if (!cfg) return;
1225
+ cfg.enabled = false;
1226
+ await cfg.onExit?.();
1227
+ try {
1228
+ const { DeviceEventEmitter } = require('react-native');
1229
+ DeviceEventEmitter.emit('yaverFeedback:dogfoodChanged', { active: false, exited: true });
1230
+ } catch { /* noop */ }
1231
+ }
1232
+
1136
1233
  /** Returns the resolved relay password the SDK is currently using.
1137
1234
  * Empty string when no relay routing is in play (direct LAN agent
1138
1235
  * URLs need no password). Callers attaching it to relay-routed
@@ -1316,6 +1413,18 @@ export class YaverFeedback {
1316
1413
  return p2pClient;
1317
1414
  }
1318
1415
 
1416
+ /** Renderer/reload route; intentionally distinct from the coding client. */
1417
+ static getRenderP2PClient(): P2PClient | null {
1418
+ return renderP2PClient ?? p2pClient;
1419
+ }
1420
+
1421
+ static getMachineRouting(): { codingDeviceId?: string; renderDeviceId?: string } {
1422
+ return {
1423
+ codingDeviceId: config?.codingDeviceId || config?.preferredDeviceId,
1424
+ renderDeviceId: config?.renderDeviceId || config?.codingDeviceId || config?.preferredDeviceId,
1425
+ };
1426
+ }
1427
+
1319
1428
  // ─── One-stop SaaS replacement methods ─────────────────────────
1320
1429
  //
1321
1430
  // These are the three solo-dev SaaS-replacement entry points
@@ -1435,6 +1544,7 @@ export class YaverFeedback {
1435
1544
  config.agentUrl = result.url;
1436
1545
  const rp = await resolveRelayPassword(config.authToken ?? '');
1437
1546
  p2pClient = new P2PClient(result.url, config.authToken ?? '', rp);
1547
+ renderP2PClient = p2pClient;
1438
1548
  }
1439
1549
  } catch {}
1440
1550
  }
@@ -1667,6 +1777,7 @@ export class YaverFeedback {
1667
1777
  enabled = false;
1668
1778
  config = null;
1669
1779
  p2pClient = null;
1780
+ renderP2PClient = null;
1670
1781
  errorBuffer = [];
1671
1782
  }
1672
1783
  }
@@ -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
  });