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.
- package/app.plugin.js +126 -9
- package/dist/BlackBox.js +9 -1
- package/dist/DeployPanel.js +65 -0
- package/dist/Discovery.js +13 -2
- package/dist/FeedbackModal.js +160 -6
- package/dist/P2PClient.d.ts +64 -1
- package/dist/P2PClient.js +222 -2
- package/dist/ShakeDetector.js +2 -0
- package/dist/YaverFeedback.d.ts +98 -0
- package/dist/YaverFeedback.js +509 -46
- package/dist/__tests__/BlackBox.relayPassword.test.d.ts +1 -0
- package/dist/__tests__/BlackBox.relayPassword.test.js +105 -0
- package/dist/__tests__/BlackBoxAutoStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStart.test.js +91 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.d.ts +1 -0
- package/dist/__tests__/BlackBoxAutoStartColdStart.test.js +156 -0
- package/dist/__tests__/BrowserLaneIcon.test.d.ts +3 -0
- package/dist/__tests__/BrowserLaneIcon.test.js +80 -0
- package/dist/__tests__/P2PClient.test.js +2 -2
- package/dist/__tests__/ReportIdentity.test.d.ts +1 -0
- package/dist/__tests__/ReportIdentity.test.js +168 -0
- package/dist/__tests__/SDKToken.test.js +1 -1
- package/dist/__tests__/ShakeToggle.test.d.ts +1 -0
- package/dist/__tests__/ShakeToggle.test.js +121 -0
- package/dist/__tests__/pickTargetDevice.test.d.ts +1 -0
- package/dist/__tests__/pickTargetDevice.test.js +89 -0
- package/dist/__tests__/reloadActions.test.d.ts +1 -0
- package/dist/__tests__/reloadActions.test.js +129 -0
- package/dist/__tests__/reloadActionsParity.test.d.ts +1 -0
- package/dist/__tests__/reloadActionsParity.test.js +42 -0
- package/dist/__tests__/types.test.js +5 -5
- package/dist/_core/device.d.ts +19 -9
- package/dist/_core/device.js +20 -14
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -1
- package/dist/reloadActions.d.ts +88 -0
- package/dist/reloadActions.js +200 -0
- package/dist/storeShots.d.ts +67 -0
- package/dist/storeShots.js +137 -0
- package/dist/types.d.ts +158 -1
- package/package.json +2 -2
- package/src/BlackBox.ts +10 -1
- package/src/DeployPanel.tsx +74 -0
- package/src/Discovery.ts +13 -2
- package/src/FeedbackModal.tsx +176 -14
- package/src/P2PClient.ts +235 -2
- package/src/ShakeDetector.ts +1 -0
- package/src/YaverFeedback.ts +498 -46
- package/src/__tests__/BlackBox.relayPassword.test.ts +129 -0
- package/src/__tests__/BlackBoxAutoStart.test.ts +111 -0
- package/src/__tests__/BlackBoxAutoStartColdStart.test.ts +191 -0
- package/src/__tests__/BrowserLaneIcon.test.ts +85 -0
- package/src/__tests__/P2PClient.test.ts +2 -2
- package/src/__tests__/ReportIdentity.test.ts +203 -0
- package/src/__tests__/SDKToken.test.ts +1 -1
- package/src/__tests__/ShakeToggle.test.ts +153 -0
- package/src/__tests__/pickTargetDevice.test.ts +101 -0
- package/src/__tests__/reloadActions.test.ts +171 -0
- package/src/__tests__/reloadActionsParity.test.ts +49 -0
- package/src/__tests__/types.test.ts +5 -5
- package/src/_core/device.ts +20 -14
- package/src/index.ts +21 -0
- package/src/reloadActions.ts +273 -0
- package/src/storeShots.ts +189 -0
- package/src/types.ts +164 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Stable identifier for each action the overlay can render. */
|
|
2
|
+
export type ReloadActionId = 'hot' | 'full' | 'rebuild';
|
|
3
|
+
/** Wire value of the `mode` field (or `bundle` for /dev/reload-app). */
|
|
4
|
+
export type ReloadWireMode = 'fast' | 'full' | 'bundle';
|
|
5
|
+
/** The part of GET /dev/status this decision depends on. */
|
|
6
|
+
export interface DevServerSnapshot {
|
|
7
|
+
/** Is a dev server process alive on the machine? */
|
|
8
|
+
running: boolean;
|
|
9
|
+
/** Is it still compiling? A reload now would race the build. */
|
|
10
|
+
building?: boolean;
|
|
11
|
+
/** Agent's framework name: expo | react-native | flutter | vite | nextjs. */
|
|
12
|
+
framework?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ReloadActionsOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Is the HOST APP a development build?
|
|
17
|
+
*
|
|
18
|
+
* There is no honest default here, so there is no default: every stack has
|
|
19
|
+
* its own signal (`__DEV__`, `kDebugMode`, `Debug.isDebugBuild`,
|
|
20
|
+
* `FLAG_DEBUGGABLE`, `#if DEBUG`) and the caller passes it. False means the
|
|
21
|
+
* list is EMPTY — a shipped app never gets a reload button.
|
|
22
|
+
*/
|
|
23
|
+
isDevBuild: boolean;
|
|
24
|
+
/** Do we have an agent connection / selected machine at all? */
|
|
25
|
+
connected: boolean;
|
|
26
|
+
/** Human label for the machine, used inside the disabled reason. */
|
|
27
|
+
machineLabel?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Offer the React Native bundle rebuild as a third action. Only the RN SDK
|
|
30
|
+
* can act on the pushed bundle, so only the RN SDK sets this.
|
|
31
|
+
*/
|
|
32
|
+
includeRebuild?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface ReloadAction {
|
|
35
|
+
id: ReloadActionId;
|
|
36
|
+
/** Button label — stack-idiomatic wording lives here, not at the call site. */
|
|
37
|
+
label: string;
|
|
38
|
+
/** One line under/next to the button explaining what it actually does. */
|
|
39
|
+
hint: string;
|
|
40
|
+
mode: ReloadWireMode;
|
|
41
|
+
/** Agent path this action POSTs to. */
|
|
42
|
+
path: string;
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Present exactly when `enabled` is false. Names the specific blocker and
|
|
46
|
+
* the fix — never "unavailable".
|
|
47
|
+
*/
|
|
48
|
+
disabledReason?: string;
|
|
49
|
+
}
|
|
50
|
+
type FrameworkFamily = 'flutter' | 'react-native' | 'web' | 'unknown';
|
|
51
|
+
/**
|
|
52
|
+
* Map the agent's framework name onto the family whose reload vocabulary we
|
|
53
|
+
* borrow. Unknown frameworks still get generic actions: the agent is the
|
|
54
|
+
* authority on what it can do, and refusing to offer a reload because we did
|
|
55
|
+
* not recognise a name would be us inventing a limit the product does not have.
|
|
56
|
+
*/
|
|
57
|
+
export declare function reloadFrameworkFamily(framework?: string): FrameworkFamily;
|
|
58
|
+
/** Path constants — mirrored from AGENT_ENDPOINTS so a typo cannot diverge. */
|
|
59
|
+
export declare const RELOAD_PATH = "/dev/reload";
|
|
60
|
+
export declare const RELOAD_APP_PATH = "/dev/reload-app";
|
|
61
|
+
/**
|
|
62
|
+
* The whole decision, in one pure function.
|
|
63
|
+
*
|
|
64
|
+
* Returns the ordered list the overlay should render. An EMPTY list means
|
|
65
|
+
* "render no reload UI at all" — that is the production-build answer, and it
|
|
66
|
+
* is deliberately indistinguishable from "this SDK has no reload feature",
|
|
67
|
+
* because to a shipped app it doesn't.
|
|
68
|
+
*
|
|
69
|
+
* A NON-empty list may still contain disabled entries: showing a greyed
|
|
70
|
+
* "Hot Reload — no dev server is running on primary" teaches the user what
|
|
71
|
+
* to fix. Hiding it teaches them nothing.
|
|
72
|
+
*/
|
|
73
|
+
export declare function reloadActions(snapshot: DevServerSnapshot | null | undefined, opts: ReloadActionsOptions): ReloadAction[];
|
|
74
|
+
/** The exact request an action makes. Kept next to the labels on purpose. */
|
|
75
|
+
export declare function reloadRequest(action: Pick<ReloadAction, 'mode' | 'path'>): {
|
|
76
|
+
method: 'POST';
|
|
77
|
+
path: string;
|
|
78
|
+
body: Record<string, unknown>;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Turn a failed reload into a sentence that names the cause AND the fix.
|
|
82
|
+
*
|
|
83
|
+
* "Reload failed" is the shape of error this codebase keeps paying whole
|
|
84
|
+
* sessions for. Every branch below exists because the raw text the agent (or
|
|
85
|
+
* Go's net stack) produces is accurate and unreadable.
|
|
86
|
+
*/
|
|
87
|
+
export declare function describeReloadFailure(status: number, body: string, snapshot?: DevServerSnapshot | null): string;
|
|
88
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Reload actions — the ONE decision seam every Yaver feedback SDK mirrors ──
|
|
3
|
+
//
|
|
4
|
+
// The in-app overlay offers the developer a way to reload the app they are
|
|
5
|
+
// looking at, without leaving it. This file answers the three questions that
|
|
6
|
+
// have to be answered IDENTICALLY on every stack, so that a bug fixed in one
|
|
7
|
+
// SDK is not still shipping in the other five:
|
|
8
|
+
//
|
|
9
|
+
// 1. WHICH actions may be shown at all (production build ⇒ none, ever)?
|
|
10
|
+
// 2. WHICH request does each action make (path + body)?
|
|
11
|
+
// 3. WHEN a reload fails, WHAT do we tell the user?
|
|
12
|
+
//
|
|
13
|
+
// It is deliberately PURE — no fetch, no DOM, no globals. That is what makes
|
|
14
|
+
// it unit-testable, and the unit test is the guard: a production build must
|
|
15
|
+
// yield an empty action list. Break `isDevBuild` and the test fails.
|
|
16
|
+
//
|
|
17
|
+
// ── Wire contract (desktop/agent/devserver_http.go) ──────────────────────────
|
|
18
|
+
//
|
|
19
|
+
// POST /dev/reload { "mode": "fast" | "full" }
|
|
20
|
+
// fast — the framework's cheapest refresh. Flutter stdin "r" (hot
|
|
21
|
+
// reload, keeps state). Metro/Expo fast refresh. Vite/Next HMR.
|
|
22
|
+
// full — framework-level restart. Flutter stdin "R" (hot RESTART,
|
|
23
|
+
// resets state). Web lane additionally forces a warm re-export.
|
|
24
|
+
// NEVER a cache clear or a process cold-start.
|
|
25
|
+
// Absent/unknown mode is normalised to "fast" by the agent, so an old
|
|
26
|
+
// client keeps its exact old behaviour.
|
|
27
|
+
//
|
|
28
|
+
// POST /dev/reload-app { "mode": "bundle", ...identity }
|
|
29
|
+
// Rebuild the Hermes bytecode bundle on the agent and push it over the
|
|
30
|
+
// BlackBox channel. React Native only, and the ONLY action that still
|
|
31
|
+
// works when no dev server is running — which is why it stays enabled
|
|
32
|
+
// in exactly that case.
|
|
33
|
+
//
|
|
34
|
+
// ── Auth ─────────────────────────────────────────────────────────────────────
|
|
35
|
+
//
|
|
36
|
+
// None of this needs a new secret. `/dev/reload` and `/dev/reload-app` are
|
|
37
|
+
// registered under `authSDKOrGuest` (desktop/agent/httpserver.go), the same
|
|
38
|
+
// middleware that already admits the bearer this SDK sends with its feedback
|
|
39
|
+
// POST. A scope-limited SDK token needs the existing `guest-reload` scope,
|
|
40
|
+
// whose path list already contains both routes — no widening, no new gate.
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.RELOAD_APP_PATH = exports.RELOAD_PATH = void 0;
|
|
43
|
+
exports.reloadFrameworkFamily = reloadFrameworkFamily;
|
|
44
|
+
exports.reloadActions = reloadActions;
|
|
45
|
+
exports.reloadRequest = reloadRequest;
|
|
46
|
+
exports.describeReloadFailure = describeReloadFailure;
|
|
47
|
+
/**
|
|
48
|
+
* Map the agent's framework name onto the family whose reload vocabulary we
|
|
49
|
+
* borrow. Unknown frameworks still get generic actions: the agent is the
|
|
50
|
+
* authority on what it can do, and refusing to offer a reload because we did
|
|
51
|
+
* not recognise a name would be us inventing a limit the product does not have.
|
|
52
|
+
*/
|
|
53
|
+
function reloadFrameworkFamily(framework) {
|
|
54
|
+
const f = (framework || '').trim().toLowerCase();
|
|
55
|
+
if (!f)
|
|
56
|
+
return 'unknown';
|
|
57
|
+
if (f.indexOf('flutter') >= 0)
|
|
58
|
+
return 'flutter';
|
|
59
|
+
if (f === 'expo' || f.indexOf('react-native') >= 0 || f.indexOf('metro') >= 0) {
|
|
60
|
+
return 'react-native';
|
|
61
|
+
}
|
|
62
|
+
if (f === 'vite' || f === 'next' || f === 'nextjs' || f === 'web' || f === 'webpack') {
|
|
63
|
+
return 'web';
|
|
64
|
+
}
|
|
65
|
+
return 'unknown';
|
|
66
|
+
}
|
|
67
|
+
const LABELS = {
|
|
68
|
+
flutter: {
|
|
69
|
+
hot: ['Hot Reload', 'Flutter hot reload (r) — keeps the current app state.'],
|
|
70
|
+
full: ['Hot Restart', 'Flutter hot restart (R) — restarts the app and resets state.'],
|
|
71
|
+
},
|
|
72
|
+
'react-native': {
|
|
73
|
+
hot: ['Hot Reload', 'Fast Refresh through Metro — keeps component state.'],
|
|
74
|
+
full: ['Full Reload', 'Reloads the whole JS bundle and resets state.'],
|
|
75
|
+
},
|
|
76
|
+
web: {
|
|
77
|
+
hot: ['Hot Reload', 'Hot module replacement through the dev server.'],
|
|
78
|
+
full: ['Full Reload', 'Re-exports the bundle and reloads the page.'],
|
|
79
|
+
},
|
|
80
|
+
unknown: {
|
|
81
|
+
hot: ['Hot Reload', "The dev server's cheapest refresh."],
|
|
82
|
+
full: ['Full Reload', 'Framework-level restart of the running app.'],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
/** Path constants — mirrored from AGENT_ENDPOINTS so a typo cannot diverge. */
|
|
86
|
+
exports.RELOAD_PATH = '/dev/reload';
|
|
87
|
+
exports.RELOAD_APP_PATH = '/dev/reload-app';
|
|
88
|
+
/**
|
|
89
|
+
* The whole decision, in one pure function.
|
|
90
|
+
*
|
|
91
|
+
* Returns the ordered list the overlay should render. An EMPTY list means
|
|
92
|
+
* "render no reload UI at all" — that is the production-build answer, and it
|
|
93
|
+
* is deliberately indistinguishable from "this SDK has no reload feature",
|
|
94
|
+
* because to a shipped app it doesn't.
|
|
95
|
+
*
|
|
96
|
+
* A NON-empty list may still contain disabled entries: showing a greyed
|
|
97
|
+
* "Hot Reload — no dev server is running on primary" teaches the user what
|
|
98
|
+
* to fix. Hiding it teaches them nothing.
|
|
99
|
+
*/
|
|
100
|
+
function reloadActions(snapshot, opts) {
|
|
101
|
+
// 1. Production build — never, under any circumstance.
|
|
102
|
+
if (!opts.isDevBuild)
|
|
103
|
+
return [];
|
|
104
|
+
const snap = snapshot || { running: false };
|
|
105
|
+
const family = reloadFrameworkFamily(snap.framework);
|
|
106
|
+
const labels = LABELS[family];
|
|
107
|
+
const machine = (opts.machineLabel || '').trim() || 'the selected machine';
|
|
108
|
+
let blocked;
|
|
109
|
+
if (!opts.connected) {
|
|
110
|
+
blocked = 'Not connected to a machine yet — pick one first.';
|
|
111
|
+
}
|
|
112
|
+
else if (snap.building) {
|
|
113
|
+
blocked = 'The dev server is still building — reload works once it finishes.';
|
|
114
|
+
}
|
|
115
|
+
else if (!snap.running) {
|
|
116
|
+
blocked =
|
|
117
|
+
`No dev server is running on ${machine}. ` +
|
|
118
|
+
'Start one from the Yaver app, or run `yaver dev start` there.';
|
|
119
|
+
}
|
|
120
|
+
const actions = [
|
|
121
|
+
{
|
|
122
|
+
id: 'hot',
|
|
123
|
+
label: labels.hot[0],
|
|
124
|
+
hint: labels.hot[1],
|
|
125
|
+
mode: 'fast',
|
|
126
|
+
path: exports.RELOAD_PATH,
|
|
127
|
+
enabled: !blocked,
|
|
128
|
+
disabledReason: blocked,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'full',
|
|
132
|
+
label: labels.full[0],
|
|
133
|
+
hint: labels.full[1],
|
|
134
|
+
mode: 'full',
|
|
135
|
+
path: exports.RELOAD_PATH,
|
|
136
|
+
enabled: !blocked,
|
|
137
|
+
disabledReason: blocked,
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
if (opts.includeRebuild) {
|
|
141
|
+
// Deliberately NOT gated on `running`: rebuilding the bundle is exactly
|
|
142
|
+
// what you do when Metro is not up. It is gated on `connected`, because
|
|
143
|
+
// without a machine there is nothing to rebuild on.
|
|
144
|
+
const rebuildBlocked = opts.connected
|
|
145
|
+
? undefined
|
|
146
|
+
: 'Not connected to a machine yet — pick one first.';
|
|
147
|
+
actions.push({
|
|
148
|
+
id: 'rebuild',
|
|
149
|
+
label: 'Rebuild Bundle',
|
|
150
|
+
hint: 'Recompiles the Hermes bundle on the machine. Works with no dev server.',
|
|
151
|
+
mode: 'bundle',
|
|
152
|
+
path: exports.RELOAD_APP_PATH,
|
|
153
|
+
enabled: !rebuildBlocked,
|
|
154
|
+
disabledReason: rebuildBlocked,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return actions;
|
|
158
|
+
}
|
|
159
|
+
/** The exact request an action makes. Kept next to the labels on purpose. */
|
|
160
|
+
function reloadRequest(action) {
|
|
161
|
+
return { method: 'POST', path: action.path, body: { mode: action.mode } };
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Turn a failed reload into a sentence that names the cause AND the fix.
|
|
165
|
+
*
|
|
166
|
+
* "Reload failed" is the shape of error this codebase keeps paying whole
|
|
167
|
+
* sessions for. Every branch below exists because the raw text the agent (or
|
|
168
|
+
* Go's net stack) produces is accurate and unreadable.
|
|
169
|
+
*/
|
|
170
|
+
function describeReloadFailure(status, body, snapshot) {
|
|
171
|
+
const lower = (body || '').toLowerCase();
|
|
172
|
+
const framework = (snapshot?.framework || '').trim();
|
|
173
|
+
if (lower.indexOf('does not support hot reload') >= 0) {
|
|
174
|
+
const name = framework || 'This dev server';
|
|
175
|
+
return `${name} cannot hot reload. Use Rebuild Bundle, or restart the dev server.`;
|
|
176
|
+
}
|
|
177
|
+
if (status === 503 ||
|
|
178
|
+
lower.indexOf('no dev server') >= 0 ||
|
|
179
|
+
lower.indexOf('dev server not available') >= 0) {
|
|
180
|
+
return 'No dev server is running on the machine. Start one before reloading.';
|
|
181
|
+
}
|
|
182
|
+
if ((lower.indexOf('connection refused') >= 0 || lower.indexOf('econnrefused') >= 0) &&
|
|
183
|
+
(lower.indexOf('127.0.0.1') >= 0 || lower.indexOf('localhost') >= 0)) {
|
|
184
|
+
return 'The dev server is not listening on the machine. Start it with `yaver dev start`.';
|
|
185
|
+
}
|
|
186
|
+
if (status === 401 || status === 403) {
|
|
187
|
+
return 'The machine rejected this session — sign in again, or re-pair this device.';
|
|
188
|
+
}
|
|
189
|
+
if (status === 404) {
|
|
190
|
+
return ('This machine’s agent has no /dev/reload route — it is too old. ' +
|
|
191
|
+
'Update it with `npm install -g yaver-cli@latest`.');
|
|
192
|
+
}
|
|
193
|
+
if (status >= 500) {
|
|
194
|
+
return 'The agent hit an internal error while reloading. Check `yaver logs` on the machine.';
|
|
195
|
+
}
|
|
196
|
+
if (status === 0) {
|
|
197
|
+
return 'Could not reach the machine. Check that it is online and `yaver serve` is running.';
|
|
198
|
+
}
|
|
199
|
+
return `Reload failed (HTTP ${status}).`;
|
|
200
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-device App Store screenshot capture (Engine 2).
|
|
3
|
+
*
|
|
4
|
+
* The desktop `yaver shots` flow drives a simulator with Maestro. This is
|
|
5
|
+
* the in-app counterpart: the SDK, living inside the real running app,
|
|
6
|
+
* walks the app's own routes and screenshots each one with
|
|
7
|
+
* `react-native-view-shot`, then uploads the frames to the Yaver agent
|
|
8
|
+
* (POST /shots/upload). The agent normalizes them and runs the same App
|
|
9
|
+
* Store Connect backend (upload → metadata → submit).
|
|
10
|
+
*
|
|
11
|
+
* Why this exists alongside the simulator path: it captures *real device*
|
|
12
|
+
* pixels, knows the exact route map (no heuristics), and the user is
|
|
13
|
+
* already authenticated — so it sidesteps the i18n / no-testID fragility
|
|
14
|
+
* that makes a blind simulator walk hard.
|
|
15
|
+
*
|
|
16
|
+
* The host wires it once by handing us a navigation ref (react-navigation
|
|
17
|
+
* or expo-router router) plus the ordered list of routes to visit.
|
|
18
|
+
*/
|
|
19
|
+
export interface StoreShotFrame {
|
|
20
|
+
route: string;
|
|
21
|
+
base64: string;
|
|
22
|
+
mimeType: string;
|
|
23
|
+
}
|
|
24
|
+
export interface CaptureStoreScreenshotsOptions {
|
|
25
|
+
/** Yaver agent base URL (e.g. http://192.168.1.5:18080 or a relay URL). */
|
|
26
|
+
agentUrl: string;
|
|
27
|
+
/** Bearer token for the agent (same-user envelope). */
|
|
28
|
+
authToken: string;
|
|
29
|
+
/** Relay password — required only when agentUrl is relay-routed. */
|
|
30
|
+
relayPassword?: string;
|
|
31
|
+
/** App name (vault scope / job label on the agent side). */
|
|
32
|
+
app: string;
|
|
33
|
+
/** iOS bundle id; the agent falls back to app.json if omitted. */
|
|
34
|
+
bundleId?: string;
|
|
35
|
+
/** App Store localization (default en-US). */
|
|
36
|
+
locale?: string;
|
|
37
|
+
/** When true, the agent also sets metadata + attempts submit-for-review. */
|
|
38
|
+
submit?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Navigation handle. Either a react-navigation ref (has `.navigate`) or
|
|
41
|
+
* an expo-router `router` (has `.push`/`.navigate`). We call the first
|
|
42
|
+
* available method with the route string.
|
|
43
|
+
*/
|
|
44
|
+
navigationRef?: any;
|
|
45
|
+
/** Ordered routes to visit + screenshot (e.g. ['/(tabs)/dashboard', ...]). */
|
|
46
|
+
routes: string[];
|
|
47
|
+
/** Optional per-route screenshot names (defaults to NN_<sanitized route>). */
|
|
48
|
+
screens?: string[];
|
|
49
|
+
/** Milliseconds to wait after navigating before capturing (default 900). */
|
|
50
|
+
settleMs?: number;
|
|
51
|
+
/** Hook the host can use to hide its own overlay before each capture. */
|
|
52
|
+
onBeforeCapture?: (route: string, index: number) => void | Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export interface CaptureStoreScreenshotsResult {
|
|
55
|
+
ok: boolean;
|
|
56
|
+
captured: number;
|
|
57
|
+
uploaded: number;
|
|
58
|
+
submitted?: boolean;
|
|
59
|
+
staged?: boolean;
|
|
60
|
+
message?: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Walk the app's routes, screenshot each, and upload the batch to the
|
|
64
|
+
* agent. Returns a summary; never throws for a single missed route —
|
|
65
|
+
* it captures what it can and reports the count.
|
|
66
|
+
*/
|
|
67
|
+
export declare function captureStoreScreenshots(opts: CaptureStoreScreenshotsOptions): Promise<CaptureStoreScreenshotsResult>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* On-device App Store screenshot capture (Engine 2).
|
|
4
|
+
*
|
|
5
|
+
* The desktop `yaver shots` flow drives a simulator with Maestro. This is
|
|
6
|
+
* the in-app counterpart: the SDK, living inside the real running app,
|
|
7
|
+
* walks the app's own routes and screenshots each one with
|
|
8
|
+
* `react-native-view-shot`, then uploads the frames to the Yaver agent
|
|
9
|
+
* (POST /shots/upload). The agent normalizes them and runs the same App
|
|
10
|
+
* Store Connect backend (upload → metadata → submit).
|
|
11
|
+
*
|
|
12
|
+
* Why this exists alongside the simulator path: it captures *real device*
|
|
13
|
+
* pixels, knows the exact route map (no heuristics), and the user is
|
|
14
|
+
* already authenticated — so it sidesteps the i18n / no-testID fragility
|
|
15
|
+
* that makes a blind simulator walk hard.
|
|
16
|
+
*
|
|
17
|
+
* The host wires it once by handing us a navigation ref (react-navigation
|
|
18
|
+
* or expo-router router) plus the ordered list of routes to visit.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.captureStoreScreenshots = captureStoreScreenshots;
|
|
22
|
+
const capture_1 = require("./capture");
|
|
23
|
+
function sleep(ms) {
|
|
24
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
25
|
+
}
|
|
26
|
+
function navigateTo(navRef, route) {
|
|
27
|
+
if (!navRef)
|
|
28
|
+
return;
|
|
29
|
+
// react-navigation ref
|
|
30
|
+
if (typeof navRef.navigate === 'function') {
|
|
31
|
+
navRef.navigate(route);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// expo-router router
|
|
35
|
+
if (typeof navRef.push === 'function') {
|
|
36
|
+
navRef.push(route);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (navRef.current && typeof navRef.current.navigate === 'function') {
|
|
40
|
+
navRef.current.navigate(route);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function shotName(route, index, override) {
|
|
45
|
+
if (override)
|
|
46
|
+
return override;
|
|
47
|
+
const clean = route
|
|
48
|
+
.replace(/^\/+/, '')
|
|
49
|
+
.replace(/[()[\]/]+/g, '_')
|
|
50
|
+
.replace(/[^a-zA-Z0-9_]+/g, '')
|
|
51
|
+
.replace(/^_+|_+$/g, '');
|
|
52
|
+
const n = String(index + 1).padStart(2, '0');
|
|
53
|
+
return `${n}_${clean || 'screen'}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Walk the app's routes, screenshot each, and upload the batch to the
|
|
57
|
+
* agent. Returns a summary; never throws for a single missed route —
|
|
58
|
+
* it captures what it can and reports the count.
|
|
59
|
+
*/
|
|
60
|
+
async function captureStoreScreenshots(opts) {
|
|
61
|
+
if (!opts.agentUrl || !opts.authToken) {
|
|
62
|
+
return { ok: false, captured: 0, uploaded: 0, message: 'agentUrl + authToken required' };
|
|
63
|
+
}
|
|
64
|
+
if (!opts.routes?.length) {
|
|
65
|
+
return { ok: false, captured: 0, uploaded: 0, message: 'no routes to capture' };
|
|
66
|
+
}
|
|
67
|
+
const settleMs = opts.settleMs ?? 900;
|
|
68
|
+
const frames = [];
|
|
69
|
+
for (let i = 0; i < opts.routes.length; i++) {
|
|
70
|
+
const route = opts.routes[i];
|
|
71
|
+
try {
|
|
72
|
+
navigateTo(opts.navigationRef, route);
|
|
73
|
+
await sleep(settleMs);
|
|
74
|
+
if (opts.onBeforeCapture)
|
|
75
|
+
await opts.onBeforeCapture(route, i);
|
|
76
|
+
const shot = await (0, capture_1.captureScreenshotBase64)();
|
|
77
|
+
if (shot?.base64) {
|
|
78
|
+
frames.push({
|
|
79
|
+
route: shotName(route, i, opts.screens?.[i]),
|
|
80
|
+
base64: shot.base64,
|
|
81
|
+
mimeType: shot.mimeType,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Skip a route that failed to render — keep walking.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (frames.length === 0) {
|
|
90
|
+
return { ok: false, captured: 0, uploaded: 0, message: 'captured no frames' };
|
|
91
|
+
}
|
|
92
|
+
const headers = {
|
|
93
|
+
Authorization: `Bearer ${opts.authToken}`,
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
};
|
|
96
|
+
if (opts.relayPassword)
|
|
97
|
+
headers['X-Relay-Password'] = opts.relayPassword;
|
|
98
|
+
const base = opts.agentUrl.replace(/\/$/, '');
|
|
99
|
+
try {
|
|
100
|
+
const resp = await fetch(`${base}/shots/upload`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers,
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
app: opts.app,
|
|
105
|
+
bundleId: opts.bundleId ?? '',
|
|
106
|
+
locale: opts.locale ?? 'en-US',
|
|
107
|
+
submit: !!opts.submit,
|
|
108
|
+
frames,
|
|
109
|
+
}),
|
|
110
|
+
});
|
|
111
|
+
const j = await resp.json().catch(() => ({}));
|
|
112
|
+
if (!resp.ok || j?.ok === false) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
captured: frames.length,
|
|
116
|
+
uploaded: 0,
|
|
117
|
+
message: j?.error || `agent /shots/upload HTTP ${resp.status}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
captured: frames.length,
|
|
123
|
+
uploaded: typeof j?.uploaded === 'number' ? j.uploaded : frames.length,
|
|
124
|
+
submitted: j?.submitted,
|
|
125
|
+
staged: j?.staged,
|
|
126
|
+
message: j?.message,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
captured: frames.length,
|
|
133
|
+
uploaded: 0,
|
|
134
|
+
message: `upload failed: ${e?.message ?? 'network error'}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|