@deeeed/metamask-harness 0.26.3 → 0.26.5
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/CHANGELOG.md +14 -0
- package/adapters/mobile/start-metro.sh +33 -0
- package/adapters/mobile/verify.sh +9 -9
- package/dist/adapters/mobile/metro-env.js +9 -0
- package/dist/adapters/mobile/prepare.js +124 -33
- package/library/actions/mobile/analytics/set_consent.mjs +79 -43
- package/library/actions/mobile/platform/bridge.mjs +7 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.26.5 - 2026-07-31
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Forward Mobile launch's resolved Metro port to every runtime leaf so stale slot context cannot start, compile, launch, or observe a second Metro.
|
|
10
|
+
- Propagate inline Segment configuration into detached Mobile Metro runs and invalidate the Metro cache when it changes.
|
|
11
|
+
- Navigate through Mobile's root settings navigator before changing analytics consent.
|
|
12
|
+
|
|
13
|
+
## 0.26.4 - 2026-07-31
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Forward Mobile verify's resolved Metro port into nested recipe runs so an older runtime context cannot redirect live bridge checks to another slot.
|
|
18
|
+
|
|
5
19
|
## 0.26.3 - 2026-07-31
|
|
6
20
|
|
|
7
21
|
### Fixed
|
|
@@ -57,6 +57,7 @@ LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
|
|
|
57
57
|
mkdir -p "$LOG_DIR"
|
|
58
58
|
LOG_FILE="$LOG_DIR/metro.log"
|
|
59
59
|
PID_FILE="$LOG_DIR/metro.pid"
|
|
60
|
+
INLINE_ENV_FILE="$LOG_DIR/metro-inline-env.sh"
|
|
60
61
|
|
|
61
62
|
# --- helpers ------------------------------------------------------------------
|
|
62
63
|
|
|
@@ -73,6 +74,24 @@ default_metro_workers() {
|
|
|
73
74
|
fi
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
write_inline_metro_env() {
|
|
78
|
+
local temporary name
|
|
79
|
+
temporary="$(mktemp "$LOG_DIR/.metro-inline-env.XXXXXX")" || return 1
|
|
80
|
+
chmod 600 "$temporary" || { rm -f "$temporary"; return 1; }
|
|
81
|
+
for name in \
|
|
82
|
+
SEGMENT_PROXY_URL \
|
|
83
|
+
SEGMENT_WRITE_KEY \
|
|
84
|
+
SEGMENT_FLUSH_INTERVAL \
|
|
85
|
+
SEGMENT_FLUSH_EVENT_LIMIT
|
|
86
|
+
do
|
|
87
|
+
if declare -p "$name" >/dev/null 2>&1; then
|
|
88
|
+
printf 'export %s=%q\n' "$name" "${!name}" >> "$temporary" \
|
|
89
|
+
|| { rm -f "$temporary"; return 1; }
|
|
90
|
+
fi
|
|
91
|
+
done
|
|
92
|
+
mv -f "$temporary" "$INLINE_ENV_FILE"
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
# Log-tail viewer window (session-safe): sourced so its run-owned-session rule is
|
|
77
96
|
# unit-testable and can never leak a window into an unrelated user session.
|
|
78
97
|
# shellcheck disable=SC1091
|
|
@@ -96,6 +115,10 @@ set +a
|
|
|
96
115
|
# build.sh remaps MM_SENTRY_DSN from MM_SENTRY_DSN_DEV for dev builds; quick-launch
|
|
97
116
|
# bypasses it, so without this Sentry never initializes in the dev client.
|
|
98
117
|
[ -n "\${MM_SENTRY_DSN:-}" ] || export MM_SENTRY_DSN="\${MM_SENTRY_DSN_DEV:-}"
|
|
118
|
+
[ ! -f $(printf '%q' "$INLINE_ENV_FILE") ] || {
|
|
119
|
+
. $(printf '%q' "$INLINE_ENV_FILE")
|
|
120
|
+
rm -f $(printf '%q' "$INLINE_ENV_FILE")
|
|
121
|
+
}
|
|
99
122
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
100
123
|
export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
|
|
101
124
|
export METRO_MAX_WORKERS="$(printf '%q' "$METRO_WORKERS")"
|
|
@@ -165,6 +188,11 @@ printf 'Starting Metro on port %s (workers=%s%s)\n' \
|
|
|
165
188
|
"$PORT" "$METRO_WORKERS" "$CLEAR_LABEL" >&2
|
|
166
189
|
printf '(Full log: %s — mm-harness logs | mm-harness logs --full)\n' "$LOG_FILE" >&2
|
|
167
190
|
|
|
191
|
+
write_inline_metro_env || {
|
|
192
|
+
printf 'start-metro: could not stage inline Metro environment\n' >&2
|
|
193
|
+
exit 1
|
|
194
|
+
}
|
|
195
|
+
|
|
168
196
|
if ! start_metro_tmux; then
|
|
169
197
|
# Metro runs detached, writing to the log. The tmux window is a read-only tail.
|
|
170
198
|
(
|
|
@@ -189,6 +217,11 @@ if ! start_metro_tmux; then
|
|
|
189
217
|
# = build.sh ios main dev); quick-launch bypasses build.sh, so without this the
|
|
190
218
|
# dev client's MM_SENTRY_DSN stays empty and Sentry never initializes.
|
|
191
219
|
[ -n "${MM_SENTRY_DSN:-}" ] || export MM_SENTRY_DSN="${MM_SENTRY_DSN_DEV:-}"
|
|
220
|
+
[ ! -f "$INLINE_ENV_FILE" ] || {
|
|
221
|
+
# shellcheck disable=SC1090
|
|
222
|
+
. "$INLINE_ENV_FILE"
|
|
223
|
+
rm -f "$INLINE_ENV_FILE"
|
|
224
|
+
}
|
|
192
225
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
193
226
|
export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
|
|
194
227
|
export METRO_MAX_WORKERS="${METRO_WORKERS}"
|
|
@@ -321,7 +321,7 @@ JSON
|
|
|
321
321
|
cd "$TARGET"
|
|
322
322
|
device_args=()
|
|
323
323
|
[ -n "$device_target" ] && device_args=(--device "$device_target")
|
|
324
|
-
"$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --target "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
|
|
324
|
+
"$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --target "$TARGET" "${device_args[@]}" --watcher-port "$port" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
|
|
325
325
|
) > "$log_path" 2>&1
|
|
326
326
|
}
|
|
327
327
|
|
|
@@ -351,6 +351,13 @@ EOF
|
|
|
351
351
|
}
|
|
352
352
|
|
|
353
353
|
if [ "$STATIC_ONLY" = false ]; then
|
|
354
|
+
port="$(watcher_port)"
|
|
355
|
+
# Reject a non-numeric port before it reaches runner arguments or the
|
|
356
|
+
# port-holder shell probe.
|
|
357
|
+
case "$port" in
|
|
358
|
+
""|*[!0-9]*) echo "Refusing mobile verify: resolved watcher port is not numeric: '$port' (check WATCHER_PORT)" >&2; exit 2 ;;
|
|
359
|
+
esac
|
|
360
|
+
|
|
354
361
|
device_target="$(mobile_device_target)"
|
|
355
362
|
if [ -z "$device_target" ]; then
|
|
356
363
|
if [ "$PLATFORM" = "android" ]; then
|
|
@@ -376,13 +383,6 @@ if [ "$STATIC_ONLY" = false ]; then
|
|
|
376
383
|
add_note "$fixture_message"
|
|
377
384
|
checks+=("$fixture_check_json")
|
|
378
385
|
|
|
379
|
-
port="$(watcher_port)"
|
|
380
|
-
# Reject a non-numeric port before it is interpolated into a shell command
|
|
381
|
-
# (port_holder_json runs `lsof -iTCP:${port}`). The .env path is regex-guarded,
|
|
382
|
-
# but the WATCHER_PORT env path is not, so validate here.
|
|
383
|
-
case "$port" in
|
|
384
|
-
""|*[!0-9]*) echo "Refusing mobile verify: resolved watcher port is not numeric: '$port' (check WATCHER_PORT)" >&2; exit 2 ;;
|
|
385
|
-
esac
|
|
386
386
|
port_holder_json "$port" > "$ARTIFACTS/logs/port-holder.json"
|
|
387
387
|
|
|
388
388
|
cat > "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" <<'JSON'
|
|
@@ -411,7 +411,7 @@ JSON
|
|
|
411
411
|
IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
|
|
412
412
|
ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
|
|
413
413
|
ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
|
|
414
|
-
"$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --target "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
|
|
414
|
+
"$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --target "$TARGET" "${device_args[@]}" --watcher-port "$port" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
|
|
415
415
|
) > "$ARTIFACTS/logs/runner-live-smoke.log" 2>&1; then
|
|
416
416
|
checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"pass\"}")
|
|
417
417
|
else
|
|
@@ -3,6 +3,12 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { recipeRuntimePath } from "../../paths.js";
|
|
5
5
|
const ENV_INPUTS = [".js.env", ".env", ".env.local"];
|
|
6
|
+
const PROCESS_ENV_INPUTS = [
|
|
7
|
+
"SEGMENT_PROXY_URL",
|
|
8
|
+
"SEGMENT_WRITE_KEY",
|
|
9
|
+
"SEGMENT_FLUSH_INTERVAL",
|
|
10
|
+
"SEGMENT_FLUSH_EVENT_LIMIT"
|
|
11
|
+
];
|
|
6
12
|
const BASELINE_FILE = "metro-env-baseline.json";
|
|
7
13
|
function mobileMetroEnvFingerprint(target) {
|
|
8
14
|
const hash = createHash("sha256");
|
|
@@ -13,6 +19,9 @@ function mobileMetroEnvFingerprint(target) {
|
|
|
13
19
|
else hash.update("<absent>");
|
|
14
20
|
hash.update("\0");
|
|
15
21
|
}
|
|
22
|
+
for (const name of PROCESS_ENV_INPUTS) {
|
|
23
|
+
hash.update(`${name}\0${process.env[name] ?? "<absent>"}\0`);
|
|
24
|
+
}
|
|
16
25
|
return hash.digest("hex");
|
|
17
26
|
}
|
|
18
27
|
function mobileMetroEnvCheck(target) {
|
|
@@ -56,20 +56,34 @@ async function prepareMobile(target, opts = {}) {
|
|
|
56
56
|
target,
|
|
57
57
|
platform,
|
|
58
58
|
json,
|
|
59
|
-
preflightMode
|
|
59
|
+
preflightMode,
|
|
60
|
+
opts.watcherPort
|
|
60
61
|
);
|
|
61
|
-
return launch2.status === 0 ? startMobileConsoleForwarder(target, platform, json) : launch2;
|
|
62
|
+
return launch2.status === 0 ? startMobileConsoleForwarder(target, platform, json, opts.watcherPort) : launch2;
|
|
62
63
|
}
|
|
63
|
-
const surface = await ensureMobileHumanSurface(
|
|
64
|
+
const surface = await ensureMobileHumanSurface(
|
|
65
|
+
target,
|
|
66
|
+
platform,
|
|
67
|
+
json,
|
|
68
|
+
opts.watcherPort
|
|
69
|
+
);
|
|
64
70
|
if (surface.status !== 0) return surface;
|
|
65
71
|
const confirm = await dispatchAction(
|
|
66
72
|
{ id: "wait-for-bridge", cwd: target, argv: ["--max-polls", String(READY_BRIDGE_CONFIRM_POLLS)] },
|
|
67
73
|
target,
|
|
68
74
|
platform,
|
|
69
75
|
json,
|
|
70
|
-
preflightMode
|
|
76
|
+
preflightMode,
|
|
77
|
+
opts.watcherPort
|
|
71
78
|
);
|
|
72
|
-
if (confirm.status === 0)
|
|
79
|
+
if (confirm.status === 0) {
|
|
80
|
+
return startMobileConsoleForwarder(
|
|
81
|
+
target,
|
|
82
|
+
platform,
|
|
83
|
+
json,
|
|
84
|
+
opts.watcherPort
|
|
85
|
+
);
|
|
86
|
+
}
|
|
73
87
|
if (!json) {
|
|
74
88
|
process.stderr.write(
|
|
75
89
|
`launch: runtime looked ready but no ${platform} bridge target answered; launching the app.
|
|
@@ -81,9 +95,10 @@ async function prepareMobile(target, opts = {}) {
|
|
|
81
95
|
target,
|
|
82
96
|
platform,
|
|
83
97
|
json,
|
|
84
|
-
preflightMode
|
|
98
|
+
preflightMode,
|
|
99
|
+
opts.watcherPort
|
|
85
100
|
);
|
|
86
|
-
return launch.status === 0 ? startMobileConsoleForwarder(target, platform, json) : launch;
|
|
101
|
+
return launch.status === 0 ? startMobileConsoleForwarder(target, platform, json, opts.watcherPort) : launch;
|
|
87
102
|
}
|
|
88
103
|
if (report.decision === "unknown") {
|
|
89
104
|
const msg = "mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>";
|
|
@@ -97,7 +112,14 @@ async function prepareMobile(target, opts = {}) {
|
|
|
97
112
|
) : report.actions,
|
|
98
113
|
restartApp
|
|
99
114
|
);
|
|
100
|
-
const actionResult = await dispatchActionSequence(
|
|
115
|
+
const actionResult = await dispatchActionSequence(
|
|
116
|
+
actions,
|
|
117
|
+
target,
|
|
118
|
+
platform,
|
|
119
|
+
json,
|
|
120
|
+
preflightMode,
|
|
121
|
+
opts.watcherPort
|
|
122
|
+
);
|
|
101
123
|
if (actionResult.status !== 0) return actionResult;
|
|
102
124
|
if (report.decision === "install" && !process.env["RECIPE_UP_INSTALL_ATTEMPTED"]) {
|
|
103
125
|
process.env["RECIPE_UP_INSTALL_ATTEMPTED"] = "1";
|
|
@@ -116,9 +138,21 @@ async function prepareMobile(target, opts = {}) {
|
|
|
116
138
|
return { status: EXIT.runtime, output: msg };
|
|
117
139
|
}
|
|
118
140
|
case "ready": {
|
|
119
|
-
const surface = await ensureMobileHumanSurface(
|
|
141
|
+
const surface = await ensureMobileHumanSurface(
|
|
142
|
+
target,
|
|
143
|
+
platform,
|
|
144
|
+
json,
|
|
145
|
+
opts.watcherPort
|
|
146
|
+
);
|
|
120
147
|
if (surface.status !== 0) return surface;
|
|
121
|
-
const bridge = await dispatchAction(
|
|
148
|
+
const bridge = await dispatchAction(
|
|
149
|
+
{ id: "wait-for-bridge", cwd: target },
|
|
150
|
+
target,
|
|
151
|
+
platform,
|
|
152
|
+
json,
|
|
153
|
+
preflightMode,
|
|
154
|
+
opts.watcherPort
|
|
155
|
+
);
|
|
122
156
|
if (bridge.status !== 0) return bridge;
|
|
123
157
|
break;
|
|
124
158
|
}
|
|
@@ -128,7 +162,8 @@ async function prepareMobile(target, opts = {}) {
|
|
|
128
162
|
target,
|
|
129
163
|
platform,
|
|
130
164
|
json,
|
|
131
|
-
preflightMode
|
|
165
|
+
preflightMode,
|
|
166
|
+
opts.watcherPort
|
|
132
167
|
);
|
|
133
168
|
if (result.status !== 0) return result;
|
|
134
169
|
break;
|
|
@@ -142,19 +177,33 @@ async function prepareMobile(target, opts = {}) {
|
|
|
142
177
|
}
|
|
143
178
|
}
|
|
144
179
|
}
|
|
145
|
-
return startMobileConsoleForwarder(
|
|
180
|
+
return startMobileConsoleForwarder(
|
|
181
|
+
target,
|
|
182
|
+
platform,
|
|
183
|
+
json,
|
|
184
|
+
opts.watcherPort
|
|
185
|
+
);
|
|
146
186
|
}
|
|
147
|
-
function startMobileConsoleForwarder(target, platform, json) {
|
|
187
|
+
function startMobileConsoleForwarder(target, platform, json, watcherPort) {
|
|
148
188
|
return dispatchAction(
|
|
149
189
|
{ id: "start-console-forwarder", cwd: target, argv: json ? ["--quiet"] : [] },
|
|
150
190
|
target,
|
|
151
191
|
platform,
|
|
152
|
-
json
|
|
192
|
+
json,
|
|
193
|
+
"fast",
|
|
194
|
+
watcherPort
|
|
153
195
|
);
|
|
154
196
|
}
|
|
155
|
-
function ensureMobileHumanSurface(target, platform, json) {
|
|
197
|
+
function ensureMobileHumanSurface(target, platform, json, watcherPort) {
|
|
156
198
|
if (platform !== "ios") return Promise.resolve({ status: 0, output: "" });
|
|
157
|
-
return dispatchAction(
|
|
199
|
+
return dispatchAction(
|
|
200
|
+
{ id: "ensure-device-ui", cwd: target },
|
|
201
|
+
target,
|
|
202
|
+
platform,
|
|
203
|
+
json,
|
|
204
|
+
"fast",
|
|
205
|
+
watcherPort
|
|
206
|
+
);
|
|
158
207
|
}
|
|
159
208
|
function withAppRestart(actions, restartApp) {
|
|
160
209
|
if (!restartApp) return actions;
|
|
@@ -162,7 +211,7 @@ function withAppRestart(actions, restartApp) {
|
|
|
162
211
|
(action) => action.id === "launch-mobile-runtime" ? { ...action, argv: [...action.argv ?? [], "--restart"] } : action
|
|
163
212
|
);
|
|
164
213
|
}
|
|
165
|
-
async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode) {
|
|
214
|
+
async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode, watcherPort) {
|
|
166
215
|
const resolved = path.resolve(target);
|
|
167
216
|
const actions = applyMobileMetroEnvPolicy(resolved, requestedActions);
|
|
168
217
|
let pendingMetroEnvFingerprint;
|
|
@@ -170,7 +219,14 @@ async function dispatchActionSequence(requestedActions, target, platform, json,
|
|
|
170
219
|
if (action.id === "start-metro") {
|
|
171
220
|
pendingMetroEnvFingerprint = mobileMetroEnvCheck(resolved).fingerprint;
|
|
172
221
|
}
|
|
173
|
-
const result = await dispatchAction(
|
|
222
|
+
const result = await dispatchAction(
|
|
223
|
+
action,
|
|
224
|
+
target,
|
|
225
|
+
platform,
|
|
226
|
+
json,
|
|
227
|
+
preflightMode,
|
|
228
|
+
watcherPort
|
|
229
|
+
);
|
|
174
230
|
if (result.status !== 0) return result;
|
|
175
231
|
if (action.id === "yarn-setup") recordDepsBaseline(resolved);
|
|
176
232
|
if (action.id === "prewarm-bundle" && pendingMetroEnvFingerprint) {
|
|
@@ -184,7 +240,7 @@ async function dispatchActionSequence(requestedActions, target, platform, json,
|
|
|
184
240
|
}
|
|
185
241
|
return { status: 0, output: "" };
|
|
186
242
|
}
|
|
187
|
-
async function dispatchAction(action, target, platform, json, preflightMode = "fast") {
|
|
243
|
+
async function dispatchAction(action, target, platform, json, preflightMode = "fast", watcherPort) {
|
|
188
244
|
const cwd = action.cwd ?? target;
|
|
189
245
|
switch (action.id) {
|
|
190
246
|
case "yarn-setup": {
|
|
@@ -194,34 +250,62 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
194
250
|
case "start-metro": {
|
|
195
251
|
const leaf = path.join(runnerDir, "adapters/mobile/start-metro.sh");
|
|
196
252
|
const extra = action.argv ?? [];
|
|
197
|
-
return spawnScriptStreaming(
|
|
253
|
+
return spawnScriptStreaming(
|
|
254
|
+
leaf,
|
|
255
|
+
withWatcherPort(["--target", cwd, ...extra], watcherPort),
|
|
256
|
+
target
|
|
257
|
+
);
|
|
198
258
|
}
|
|
199
259
|
case "start-console-forwarder": {
|
|
200
260
|
const leaf = path.join(runnerDir, "adapters/mobile/start-console-forwarder.sh");
|
|
201
|
-
return spawnScriptStreaming(
|
|
261
|
+
return spawnScriptStreaming(
|
|
262
|
+
leaf,
|
|
263
|
+
withWatcherPort(
|
|
264
|
+
["--target", cwd, ...action.argv ?? []],
|
|
265
|
+
watcherPort
|
|
266
|
+
),
|
|
267
|
+
target
|
|
268
|
+
);
|
|
202
269
|
}
|
|
203
270
|
case "prewarm-bundle": {
|
|
204
271
|
const leaf = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
|
|
205
|
-
return spawnScriptStreaming(
|
|
272
|
+
return spawnScriptStreaming(
|
|
273
|
+
leaf,
|
|
274
|
+
withWatcherPort(
|
|
275
|
+
["--platform", platform, "--target", cwd, ...action.argv ?? []],
|
|
276
|
+
watcherPort
|
|
277
|
+
),
|
|
278
|
+
target
|
|
279
|
+
);
|
|
206
280
|
}
|
|
207
281
|
case "wait-for-bridge": {
|
|
208
282
|
const leaf = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
|
|
209
283
|
const extra = action.argv ?? [];
|
|
210
|
-
return spawnScriptStreaming(
|
|
284
|
+
return spawnScriptStreaming(
|
|
285
|
+
leaf,
|
|
286
|
+
withWatcherPort(
|
|
287
|
+
["--target", cwd, "--platform", platform, ...extra],
|
|
288
|
+
watcherPort
|
|
289
|
+
),
|
|
290
|
+
target
|
|
291
|
+
);
|
|
211
292
|
}
|
|
212
293
|
case "launch-mobile-runtime": {
|
|
213
294
|
const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
|
|
214
295
|
return spawnScriptStreaming(
|
|
215
296
|
leaf,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
297
|
+
withWatcherPort(
|
|
298
|
+
[
|
|
299
|
+
"--platform",
|
|
300
|
+
platform,
|
|
301
|
+
"--target",
|
|
302
|
+
cwd,
|
|
303
|
+
"--preflight-mode",
|
|
304
|
+
preflightMode,
|
|
305
|
+
...action.argv ?? []
|
|
306
|
+
],
|
|
307
|
+
watcherPort
|
|
308
|
+
),
|
|
225
309
|
target,
|
|
226
310
|
POD_PROBE_ENV
|
|
227
311
|
);
|
|
@@ -230,7 +314,10 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
230
314
|
const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
|
|
231
315
|
return spawnScriptStreaming(
|
|
232
316
|
leaf,
|
|
233
|
-
|
|
317
|
+
withWatcherPort(
|
|
318
|
+
["--platform", platform, "--target", cwd, "--ui-only"],
|
|
319
|
+
watcherPort
|
|
320
|
+
),
|
|
234
321
|
target,
|
|
235
322
|
POD_PROBE_ENV
|
|
236
323
|
);
|
|
@@ -245,6 +332,10 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
245
332
|
return { status: 0, output: "" };
|
|
246
333
|
}
|
|
247
334
|
}
|
|
335
|
+
function withWatcherPort(args, watcherPort) {
|
|
336
|
+
if (watcherPort === void 0 || args.includes("--port")) return args;
|
|
337
|
+
return [...args, "--port", String(watcherPort)];
|
|
338
|
+
}
|
|
248
339
|
export {
|
|
249
340
|
mobileRuntimeStatus,
|
|
250
341
|
prepareMobile
|
|
@@ -6,11 +6,8 @@ const SWITCH_IDS = {
|
|
|
6
6
|
marketing: 'data-collection-switch',
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
function toggleExpression(testId, expected,
|
|
10
|
-
return `(
|
|
11
|
-
const deadline = Date.now() + ${JSON.stringify(timeoutMs)};
|
|
12
|
-
let invoked = false;
|
|
13
|
-
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
function toggleExpression(testId, expected, invoke) {
|
|
10
|
+
return `(() => {
|
|
14
11
|
const find = () => {
|
|
15
12
|
const hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
16
13
|
const rootsFor = hook?.getFiberRoots;
|
|
@@ -28,57 +25,96 @@ function toggleExpression(testId, expected, timeoutMs) {
|
|
|
28
25
|
}
|
|
29
26
|
return null;
|
|
30
27
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
await props.onValueChange(${JSON.stringify(expected)});
|
|
43
|
-
}
|
|
44
|
-
await delay(100);
|
|
28
|
+
const target = find();
|
|
29
|
+
const props = target?.memoizedProps;
|
|
30
|
+
if (!props) return { matched: false, found: false, invoked: false };
|
|
31
|
+
if (Boolean(props.value) === ${JSON.stringify(expected)}) {
|
|
32
|
+
return { matched: true, found: true, invoked: false };
|
|
33
|
+
}
|
|
34
|
+
if (!${JSON.stringify(invoke)}) {
|
|
35
|
+
return { matched: false, found: true, invoked: false };
|
|
36
|
+
}
|
|
37
|
+
if (typeof props.onValueChange !== 'function') {
|
|
38
|
+
throw new Error('Consent switch has no onValueChange handler: ${testId}');
|
|
45
39
|
}
|
|
46
|
-
|
|
40
|
+
return Promise.resolve(
|
|
41
|
+
props.onValueChange(${JSON.stringify(expected)})
|
|
42
|
+
).then(() => ({
|
|
43
|
+
matched: false,
|
|
44
|
+
found: true,
|
|
45
|
+
invoked: true
|
|
46
|
+
}));
|
|
47
47
|
})()`;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function stateExpression(participate, marketing
|
|
51
|
-
return `(
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
if (
|
|
50
|
+
function stateExpression(participate, marketing) {
|
|
51
|
+
return `(() => {
|
|
52
|
+
const state = globalThis.store?.getState?.();
|
|
53
|
+
const analytics = state?.engine?.backgroundState?.AnalyticsController ?? {};
|
|
54
|
+
const consent = {
|
|
55
|
+
optedIn: Boolean(analytics.optedIn),
|
|
56
|
+
dataCollectionForMarketing: Boolean(state?.security?.dataCollectionForMarketing),
|
|
57
|
+
analyticsId: analytics.analyticsId ? 'set' : null
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
ready:
|
|
63
61
|
consent.optedIn === ${JSON.stringify(participate)} &&
|
|
64
62
|
consent.dataCollectionForMarketing === ${JSON.stringify(marketing)} &&
|
|
65
|
-
(!${JSON.stringify(participate)} || consent.analyticsId === 'set')
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
throw new Error('Timed out reading back Mobile analytics consent.');
|
|
63
|
+
(!${JSON.stringify(participate)} || consent.analyticsId === 'set'),
|
|
64
|
+
consent
|
|
65
|
+
};
|
|
70
66
|
})()`;
|
|
71
67
|
}
|
|
72
68
|
|
|
69
|
+
function delay(ms) {
|
|
70
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function setSwitch(input, testId, expected, timeoutMs) {
|
|
74
|
+
const deadline = Date.now() + timeoutMs;
|
|
75
|
+
let invoked = false;
|
|
76
|
+
while (Date.now() < deadline) {
|
|
77
|
+
const result = await evalAsync(
|
|
78
|
+
input,
|
|
79
|
+
toggleExpression(testId, expected, !invoked),
|
|
80
|
+
);
|
|
81
|
+
invoked ||= Boolean(result?.invoked);
|
|
82
|
+
if (result?.matched) {
|
|
83
|
+
return { ...result, changed: invoked };
|
|
84
|
+
}
|
|
85
|
+
await delay(100);
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Timed out setting consent switch ${testId} to ${expected}.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function readConsent(input, participate, marketing, timeoutMs) {
|
|
91
|
+
const deadline = Date.now() + timeoutMs;
|
|
92
|
+
while (Date.now() < deadline) {
|
|
93
|
+
const result = await evalAsync(
|
|
94
|
+
input,
|
|
95
|
+
stateExpression(participate, marketing),
|
|
96
|
+
);
|
|
97
|
+
if (result?.ready) {
|
|
98
|
+
return result.consent;
|
|
99
|
+
}
|
|
100
|
+
await delay(100);
|
|
101
|
+
}
|
|
102
|
+
throw new Error('Timed out reading back Mobile analytics consent.');
|
|
103
|
+
}
|
|
104
|
+
|
|
73
105
|
runAdapter(async (input) => {
|
|
74
106
|
const { participate, marketing, timeoutMs } = consentParams(input.node);
|
|
75
107
|
|
|
76
|
-
const navigation = await navigate(input, '
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
108
|
+
const navigation = await navigate(input, 'SettingsView', {
|
|
109
|
+
screen: 'SecuritySettings',
|
|
110
|
+
}, 'SecuritySettings');
|
|
111
|
+
await setSwitch(input, SWITCH_IDS.participate, participate, timeoutMs);
|
|
112
|
+
await setSwitch(input, SWITCH_IDS.marketing, marketing, timeoutMs);
|
|
113
|
+
const consent = await readConsent(
|
|
80
114
|
input,
|
|
81
|
-
|
|
115
|
+
participate,
|
|
116
|
+
marketing,
|
|
117
|
+
timeoutMs,
|
|
82
118
|
);
|
|
83
119
|
|
|
84
120
|
return {
|
|
@@ -355,11 +355,14 @@ export async function evalSync(input, expression) {
|
|
|
355
355
|
return parseMaybeJson(await bridgeCommand(input, ['eval', expression]));
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
export async function navigate(input, route, params = {}) {
|
|
358
|
+
export async function navigate(input, route, params = {}, expectedRoute) {
|
|
359
359
|
const navigation = await bridgeCommand(input, ['navigate', route, JSON.stringify(params)]);
|
|
360
|
-
const verifiedRoute =
|
|
361
|
-
|
|
362
|
-
|
|
360
|
+
const verifiedRoute = String(
|
|
361
|
+
expectedRoute ??
|
|
362
|
+
(navigation && typeof navigation === 'object' && navigation.navigated
|
|
363
|
+
? navigation.navigated
|
|
364
|
+
: route),
|
|
365
|
+
);
|
|
363
366
|
const currentRoute = await waitForRoute(input, verifiedRoute, Number(input.node?.navigation_timeout_ms ?? 15000));
|
|
364
367
|
return { ...navigation, currentRoute, verifiedRoute };
|
|
365
368
|
}
|