@swipt/cli 0.1.0-nightly.46 → 0.1.0-nightly.48

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/sideload/trust.js CHANGED
@@ -1,235 +1,249 @@
1
- const [id] = args;
1
+ const [id, mode, waitsForUnlock] = args;
2
+ const name = "Swipt";
2
3
  const settings = "com.apple.Preferences";
4
+ const springboard = "com.apple.springboard";
3
5
  const developerPattern =
4
6
  /(?:Apple Development|iPhone Developer): .+?\([A-Z0-9]{10}\)/;
5
- const tapTimeoutMs = 3_000;
6
7
 
7
- const sheet = async () => {
8
- const screen = await accessibility.snapshot({ scope: "screen" });
9
- return screen.find({ role: "sheet" });
10
- };
8
+ const sheet = (timeoutMs) =>
9
+ accessibility.find(
10
+ { app: springboard, role: "sheet", label: /./ },
11
+ { timeoutMs, poll: true },
12
+ );
11
13
 
12
- const alertPresentMs = 600;
14
+ const inSettings = (selector, timeoutMs) =>
15
+ accessibility.find({ app: settings, ...selector }, { timeoutMs, poll: true });
13
16
 
14
- async function dismiss(alert, buttons, index) {
15
- await sleep(alertPresentMs);
16
- for (let attempt = 0; attempt < 5; attempt++) {
17
- await input.tap(buttons[index].point);
18
- await sleep(alertPresentMs);
19
- const shown = await sheet();
20
- if (shown === null || shown.label !== alert.label) {
21
- return;
17
+ const escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18
+
19
+ async function tap(element) {
20
+ try {
21
+ await accessibility.tap(element, { timeoutMs: 0 });
22
+ } catch (error) {
23
+ if (error.name !== "TimeoutError") {
24
+ throw error;
22
25
  }
23
26
  }
24
- throw new Error(`the alert stays: ${alert.label}`);
25
27
  }
26
28
 
27
- async function allowPrompts(name, waitMs) {
28
- const until = Date.now() + waitMs;
29
+ async function dismiss(alert, index) {
30
+ const until = Date.now() + 10_000;
31
+ let absent = 0;
29
32
  do {
30
- const alert = await sheet();
31
- const buttons = alert?.findAll({ role: "button" }) ?? [];
32
- if (buttons.length === 2 && alert.label?.includes(name)) {
33
- await dismiss(alert, buttons, 1);
33
+ const shown = await sheet();
34
+ if (shown === null) {
35
+ absent += 1;
36
+ if (absent === 2) {
37
+ return;
38
+ }
39
+ continue;
40
+ }
41
+ if (shown.label !== alert.label) {
34
42
  return;
35
43
  }
36
- await sleep(300);
44
+ absent = 0;
45
+ await tap(shown.findAll({ role: "button" })[index]);
37
46
  } while (Date.now() < until);
47
+ throw new Error(`the alert stays: ${alert.label}`);
38
48
  }
39
49
 
40
- async function launch(name) {
50
+ async function answerPrompt() {
51
+ const alert = await sheet();
52
+ const buttons = alert?.findAll({ role: "button" }) ?? [];
53
+ if (alert?.label?.includes(name) && [2, 3].includes(buttons.length)) {
54
+ console.debug("answering a permission alert");
55
+ // The location alert shows an icon and lists Allow Once first; the cellular
56
+ // data alert of China mainland devices shows none and lists Wi-Fi and
57
+ // cellular first (iPhone18,2, iOS 27.2 24B115).
58
+ const location = alert.findAll({ role: "image" }).length > 0;
59
+ await dismiss(alert, buttons.length === 2 || location ? 1 : 0);
60
+ }
61
+ return alert !== null;
62
+ }
63
+
64
+ async function launch() {
41
65
  try {
66
+ console.debug(`launching ${name}`);
42
67
  await device.launchApplication(id, { restart: true });
43
- await allowPrompts(name, 3_000);
44
68
  return null;
45
69
  } catch (error) {
46
- for (let attempt = 0; attempt < 10; attempt++) {
47
- const alert = await sheet();
70
+ const until = Date.now() + 5_000;
71
+ do {
72
+ const alert = await sheet(Math.max(until - Date.now(), 0));
48
73
  const buttons = alert?.findAll({ role: "button" }) ?? [];
49
74
  if (buttons.length === 1) {
50
75
  const developer = alert
51
76
  .findAll({ role: "text" })
52
77
  .map((text) => developerPattern.exec(text.label ?? "")?.[0])
53
78
  .find((found) => found !== undefined);
54
- await dismiss(alert, buttons, 0);
79
+ await dismiss(alert, 0);
55
80
  if (developer === undefined) {
56
81
  throw new Error(`the alert names no developer: ${alert.label}`);
57
82
  }
58
83
  return developer;
59
84
  }
60
- await sleep(300);
61
- }
85
+ } while (Date.now() < until);
62
86
  throw error;
63
87
  }
64
88
  }
65
89
 
66
- async function findScrolling(selector, bounds) {
67
- for (let attempt = 0; attempt < 12; attempt++) {
68
- const found = await accessibility.find(
69
- { app: settings, ...selector },
70
- { timeoutMs: 1_500, poll: true },
71
- );
72
- if (found !== null && !found.partial && !found.offScreen) {
73
- return found;
74
- }
75
- const distance = found === null ? 0.5 : 0.25;
76
- await input.swipe(
77
- [bounds.width * 0.6, bounds.height * (0.3 + distance)],
78
- [bounds.width * 0.6, bounds.height * 0.3],
79
- { durationMs: 300 },
80
- );
81
- await sleep(500);
90
+ function developerRows(list, developer) {
91
+ if (list.findAll({ role: "button", identifier: "VPN" }).length === 0) {
92
+ return [];
82
93
  }
83
- throw new Error(`Settings did not show ${JSON.stringify(selector)}`);
94
+ const elsewhere = new Set(
95
+ list
96
+ .findAll({
97
+ role: "button",
98
+ within: {
99
+ identifier: "com.apple.settings.sidebar.collectionView",
100
+ },
101
+ })
102
+ .concat(
103
+ list.findAll({
104
+ role: "button",
105
+ within: { role: /^(navigationBar|searchField)$/ },
106
+ }),
107
+ )
108
+ .map((row) => row.identifier),
109
+ );
110
+ return list
111
+ .findAll({ role: "button" })
112
+ .filter(
113
+ (row) =>
114
+ row.identifier !== undefined &&
115
+ row.identifier !== "BackButton" &&
116
+ row.identifier !== "VPN" &&
117
+ !row.identifier.startsWith("com.apple.settings.") &&
118
+ row.identifier !== row.label &&
119
+ !elsewhere.has(row.identifier),
120
+ )
121
+ .sort(
122
+ (a, b) =>
123
+ developer.includes(b.identifier) - developer.includes(a.identifier),
124
+ );
84
125
  }
85
126
 
86
- async function openRow(selector, bounds, loaded) {
87
- for (let attempt = 0; attempt < 3; attempt++) {
88
- try {
89
- await accessibility.tap(await findScrolling(selector, bounds), {
90
- timeoutMs: tapTimeoutMs,
91
- });
92
- } catch (error) {
93
- if (error.name !== "TimeoutError") {
94
- throw error;
95
- }
96
- await input.pressHome();
97
- await sleep(800);
98
- await device.launchApplication(settings);
99
- await sleep(1_000);
100
- continue;
101
- }
102
- for (let wait = 0; wait < 10; wait++) {
103
- await sleep(300);
104
- if (loaded(await accessibility.snapshot({ scope: "frontApp" }))) {
105
- return;
106
- }
127
+ async function openDeviceManagement(developer) {
128
+ await device.launchApplication(settings, {
129
+ url: "prefs:root=General&path=ManagedConfigurationList",
130
+ });
131
+ const until = Date.now() + 10_000;
132
+ do {
133
+ const list = await accessibility.snapshot({ scope: "frontApp" });
134
+ const rows = developerRows(list, developer);
135
+ if (rows.length > 0) {
136
+ return rows;
107
137
  }
108
- }
109
- throw new Error(`Settings did not open ${JSON.stringify(selector)}`);
138
+ await answerPrompt();
139
+ } while (Date.now() < until);
140
+ throw new Error(`Settings lists no app from ${developer}`);
110
141
  }
111
142
 
112
- async function back() {
113
- const button = await accessibility.find(
114
- { app: settings, identifier: "BackButton" },
115
- { timeoutMs: 3_000, poll: true },
116
- );
117
- if (button === null) {
118
- throw new Error("Settings shows no back button");
143
+ async function allowDeveloper(row) {
144
+ const control = {
145
+ role: "text",
146
+ label: new RegExp(
147
+ `.${escaped(row.identifier)}|${escaped(row.identifier)}.`,
148
+ ),
149
+ };
150
+ if ((await inSettings(control, 1_500)) === null) {
151
+ return false;
119
152
  }
120
- await accessibility.tap(button, { timeoutMs: tapTimeoutMs });
121
- await sleep(600);
153
+ const until = Date.now() + 5_000;
154
+ do {
155
+ const shown = await inSettings(control);
156
+ if (shown !== null) {
157
+ await input.tap(shown.point);
158
+ }
159
+ const alert = await sheet();
160
+ const buttons = alert?.findAll({ role: "button" }) ?? [];
161
+ if (buttons.length === 2) {
162
+ await dismiss(alert, 0);
163
+ while (
164
+ (await inSettings(control)) !== null &&
165
+ Date.now() < until + 10_000
166
+ );
167
+ return true;
168
+ }
169
+ } while (Date.now() < until);
170
+ throw new Error("Settings did not ask to allow the developer");
122
171
  }
123
172
 
124
- async function trust(developer, bounds) {
125
- await device.launchApplication(settings, { restart: true });
126
- await openRow(
127
- { identifier: "com.apple.settings.general" },
128
- bounds,
129
- (opened) =>
130
- opened.findAll({ identifier: /^com\.apple\.settings\.general\./ })
131
- .length > 0,
132
- );
133
- await openRow(
134
- { identifier: "com.apple.settings.general.vpnAndDeviceManagement" },
135
- bounds,
136
- (opened) =>
137
- opened.findAll({ role: "button", identifier: "VPN" }).length > 0,
138
- );
139
- const candidates = (list) => {
140
- const elsewhere = new Set(
141
- list
142
- .findAll({
143
- role: "button",
144
- within: {
145
- identifier: "com.apple.settings.sidebar.collectionView",
146
- },
147
- })
148
- .concat(
149
- list.findAll({
150
- role: "button",
151
- within: { role: /^(navigationBar|searchField)$/ },
152
- }),
153
- )
154
- .map((row) => row.identifier),
155
- );
156
- return list
157
- .findAll({ role: "button" })
158
- .filter(
159
- (row) =>
160
- row.identifier !== undefined &&
161
- row.identifier !== "BackButton" &&
162
- row.identifier !== "VPN" &&
163
- row.identifier !== row.label &&
164
- !elsewhere.has(row.identifier),
165
- )
166
- .map((row) => row.identifier);
167
- };
168
- let rows = [];
169
- for (let attempt = 0; attempt < 15 && rows.length === 0; attempt++) {
170
- await sleep(400);
171
- rows = candidates(await accessibility.snapshot({ scope: "frontApp" }));
172
- }
173
- for (const row of rows) {
174
- const button = await accessibility.find(
175
- { app: settings, role: "button", identifier: row },
176
- { timeoutMs: 3_000, poll: true },
177
- );
173
+ async function trust(developer) {
174
+ console.debug(`trusting ${developer} in Settings`);
175
+ const page = { label: new RegExp(escaped(developer)) };
176
+ for (const row of await openDeviceManagement(developer)) {
177
+ const until = Date.now() + 3_000;
178
+ let opened = null;
179
+ do {
180
+ await tap(row);
181
+ opened = await inSettings(page);
182
+ } while (opened === null && Date.now() < until);
183
+ if (opened !== null && (await allowDeveloper(row))) {
184
+ return;
185
+ }
186
+ const button = await inSettings({ identifier: "BackButton" }, 3_000);
178
187
  if (button === null) {
179
- continue;
188
+ throw new Error("Settings shows no back button");
180
189
  }
181
- await accessibility.tap(button, { timeoutMs: tapTimeoutMs });
182
- let control = null;
183
- for (let attempt = 0; attempt < 10 && control === null; attempt++) {
184
- await sleep(300);
185
- const opened = await accessibility.snapshot({ scope: "frontApp" });
186
- if (
187
- opened.findAll((it) => (it.label ?? "").includes(developer)).length > 0
188
- ) {
189
- control =
190
- opened
191
- .findAll({ role: "text" })
192
- .find((it) => it.label !== row && it.label?.includes(row)) ?? null;
193
- break;
190
+ await tap(button);
191
+ }
192
+ throw new Error(`Settings lists no app from ${developer}`);
193
+ }
194
+
195
+ async function waitForUnlock() {
196
+ let announced = false;
197
+ for (;;) {
198
+ try {
199
+ if (!(await device.locked.get())) {
200
+ return;
201
+ }
202
+ } catch (error) {
203
+ if (error.name !== "UnavailableError") {
204
+ throw error;
194
205
  }
195
206
  }
196
- if (control === null) {
197
- await back();
198
- continue;
207
+ if (!announced) {
208
+ console.debug("unlock the device to continue");
209
+ announced = true;
199
210
  }
200
- await input.tap(control.point);
201
- for (let attempt = 0; attempt < 10; attempt++) {
202
- const alert = await sheet();
203
- const buttons = alert?.findAll({ role: "button" }) ?? [];
204
- if (buttons.length === 2) {
205
- await dismiss(alert, buttons, 0);
211
+ }
212
+ }
213
+
214
+ async function finishSetup() {
215
+ const until = Date.now() + 60_000;
216
+ do {
217
+ if (!(await answerPrompt())) {
218
+ const finished = await accessibility.find({
219
+ app: id,
220
+ identifier: "setup-finished",
221
+ });
222
+ if (finished !== null) {
223
+ console.log(`${name} is running`);
206
224
  return;
207
225
  }
208
- await sleep(300);
209
226
  }
210
- throw new Error("Settings did not ask to allow the developer");
211
- }
212
- throw new Error(`Settings lists no app from ${developer}`);
227
+ } while (Date.now() < until);
228
+ throw new Error(`${name} did not finish setting up`);
213
229
  }
214
230
 
215
231
  async function main() {
216
- await device.unlock().catch(() => {});
217
- const name =
218
- (await device.installedApplications()).find((app) => app.bundleId === id)
219
- ?.name ?? "Swipt";
220
- await allowPrompts(name, 0);
221
- const developer = await launch(name);
222
- if (developer === null) {
223
- console.log(`${name} is running`);
232
+ if (mode === "prompts") {
233
+ await finishSetup();
224
234
  return;
225
235
  }
226
- const { screen: bounds } = await device.info();
227
- await trust(developer, bounds);
228
- await sleep(1_000);
229
- if ((await launch(name)) !== null) {
230
- throw new Error(`${name} does not open after trusting ${developer}`);
236
+ console.debug("unlocking the device");
237
+ await device.unlock().catch(() => {});
238
+ if (waitsForUnlock === "wait") {
239
+ await waitForUnlock();
240
+ }
241
+ await answerPrompt();
242
+ const developer = await launch();
243
+ if (developer !== null) {
244
+ await trust(developer);
245
+ console.log(`${developer} is trusted`);
231
246
  }
232
- console.log(`${name} is running; ${developer} is trusted`);
233
247
  }
234
248
 
235
249
  await main();