@limrun/ui 0.9.0-rc.1 → 0.9.0-rc.10
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/dist/components/inspect-overlay.d.ts +33 -0
- package/dist/components/remote-control.d.ts +86 -0
- package/dist/core/ax-fetcher.d.ts +49 -0
- package/dist/core/ax-tree.d.ts +99 -0
- package/dist/core/device-install/apple/client.d.ts +1 -0
- package/dist/core/device-install/apple/provisioning.d.ts +42 -31
- package/dist/core/device-install/apple/relay.d.ts +5 -9
- package/dist/core/device-install/storage/browser-storage.d.ts +19 -0
- package/dist/core/device-install/types.d.ts +2 -2
- package/dist/device-install/index.cjs +1 -9
- package/dist/device-install/index.js +76 -210
- package/dist/device-install/react.cjs +1 -1
- package/dist/device-install/react.js +1 -1
- package/dist/device-install-dialog-DJkKg8y6.mjs +443 -0
- package/dist/device-install-dialog-DfRemlSg.js +2 -0
- package/dist/device-install-dialog.css +1 -1
- package/dist/hooks/use-device-install.d.ts +21 -3
- package/dist/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1485 -778
- package/dist/use-device-install-CS201taa.js +31 -0
- package/dist/use-device-install-jz1BMMQx.mjs +13627 -0
- package/package.json +7 -3
- package/src/components/device-install/device-install-dialog.css +82 -1
- package/src/components/device-install/device-install-dialog.tsx +319 -187
- package/src/components/inspect-overlay.css +223 -0
- package/src/components/inspect-overlay.tsx +437 -0
- package/src/components/remote-control.tsx +547 -9
- package/src/core/ax-fetcher.test.ts +418 -0
- package/src/core/ax-fetcher.ts +377 -0
- package/src/core/ax-tree.test.ts +491 -0
- package/src/core/ax-tree.ts +416 -0
- package/src/core/device-install/apple/client.ts +92 -4
- package/src/core/device-install/apple/provisioning.ts +67 -24
- package/src/core/device-install/apple/relay.ts +121 -205
- package/src/core/device-install/operations/limbuild-client.ts +1 -1
- package/src/core/device-install/storage/browser-storage.ts +26 -1
- package/src/core/device-install/types.ts +2 -2
- package/src/demo.tsx +93 -10
- package/src/hooks/use-device-install.ts +766 -67
- package/src/index.ts +19 -1
- package/vitest.config.ts +23 -0
- package/dist/device-install-dialog-CTwVViYY.js +0 -2
- package/dist/device-install-dialog-zzKJu7SM.mjs +0 -328
- package/dist/use-device-install-CgrOKKyi.mjs +0 -13042
- package/dist/use-device-install-DDKRf6IL.js +0 -23
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useId, useState, type ChangeEvent, type ReactNode } from 'react';
|
|
1
|
+
import { useEffect, useId, useState, type ChangeEvent, type ReactNode } from 'react';
|
|
2
2
|
import { clsx } from 'clsx';
|
|
3
3
|
import { useDeviceInstall, type UseDeviceInstallOptions } from '../../hooks/use-device-install';
|
|
4
4
|
import type { DeviceInstallStep, DeviceInstallStepStatus } from '../../core/device-install';
|
|
@@ -10,19 +10,19 @@ export type DeviceInstallDialogProps = UseDeviceInstallOptions & {
|
|
|
10
10
|
|
|
11
11
|
const steps: Array<{ id: DeviceInstallStep; title: string; description: string }> = [
|
|
12
12
|
{
|
|
13
|
-
id: '
|
|
14
|
-
title: '
|
|
15
|
-
description: '
|
|
13
|
+
id: 'signing',
|
|
14
|
+
title: 'Prepare signing',
|
|
15
|
+
description: 'Choose Apple ID login or upload certificates for a registered developer device.',
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
|
-
id: '
|
|
19
|
-
title: '
|
|
20
|
-
description: '
|
|
18
|
+
id: 'build',
|
|
19
|
+
title: 'Build for device',
|
|
20
|
+
description: 'Start the signed iPhone build before connecting over USB.',
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
|
-
id: '
|
|
24
|
-
title: '
|
|
25
|
-
description: '
|
|
23
|
+
id: 'connect',
|
|
24
|
+
title: 'Connect and pair',
|
|
25
|
+
description: 'After the build succeeds, connect the iPhone with WebUSB and pair this browser.',
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
id: 'install',
|
|
@@ -31,17 +31,25 @@ const steps: Array<{ id: DeviceInstallStep; title: string; description: string }
|
|
|
31
31
|
},
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
+
type SigningSection = 'apple-id' | 'upload';
|
|
35
|
+
|
|
34
36
|
export function DeviceInstallDialog({
|
|
35
37
|
disabled,
|
|
36
38
|
...hookOptions
|
|
37
39
|
}: DeviceInstallDialogProps) {
|
|
38
40
|
const [open, setOpen] = useState(false);
|
|
41
|
+
const [openStep, setOpenStep] = useState<DeviceInstallStep>('signing');
|
|
42
|
+
const [signingSection, setSigningSection] = useState<SigningSection>();
|
|
39
43
|
const [appleAccountName, setAppleAccountName] = useState('');
|
|
40
44
|
const [applePassword, setApplePassword] = useState('');
|
|
41
45
|
const [appleTwoFactorCode, setAppleTwoFactorCode] = useState('');
|
|
42
46
|
const dialogTitleId = useId();
|
|
43
47
|
const deviceInstall = useDeviceInstall(hookOptions);
|
|
44
48
|
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
setOpenStep(deviceInstall.currentStep);
|
|
51
|
+
}, [deviceInstall.currentStep]);
|
|
52
|
+
|
|
45
53
|
const updateSigningFiles = (field: 'certificateFile' | 'provisioningProfileFile', event: ChangeEvent<HTMLInputElement>) => {
|
|
46
54
|
deviceInstall.setSigningFiles({
|
|
47
55
|
[field]: event.currentTarget.files?.[0],
|
|
@@ -70,7 +78,7 @@ export function DeviceInstallDialog({
|
|
|
70
78
|
<header className="lr-device-install__header">
|
|
71
79
|
<div>
|
|
72
80
|
<h2 id={dialogTitleId}>Install to a real iPhone</h2>
|
|
73
|
-
<p>
|
|
81
|
+
<p>Prepare signing, build for the registered device, connect and pair, then install from this browser.</p>
|
|
74
82
|
</div>
|
|
75
83
|
<button type="button" className="lr-device-install__icon-button" onClick={() => setOpen(false)}>
|
|
76
84
|
Close
|
|
@@ -86,182 +94,262 @@ export function DeviceInstallDialog({
|
|
|
86
94
|
index={index + 1}
|
|
87
95
|
step={step}
|
|
88
96
|
active={deviceInstall.currentStep === step.id}
|
|
97
|
+
open={openStep === step.id}
|
|
89
98
|
status={deviceInstall.stepStatuses[step.id]}
|
|
99
|
+
onToggle={() => setOpenStep(step.id)}
|
|
90
100
|
>
|
|
91
|
-
{step.id === '
|
|
101
|
+
{step.id === 'signing' && (
|
|
92
102
|
<div className="lr-device-install__step-body">
|
|
93
|
-
<div className="lr-device-
|
|
94
|
-
<label className="lr-device-install__field">
|
|
95
|
-
<span>Apple ID</span>
|
|
96
|
-
<input
|
|
97
|
-
type="email"
|
|
98
|
-
autoComplete="username"
|
|
99
|
-
placeholder="name@example.com"
|
|
100
|
-
value={appleAccountName}
|
|
101
|
-
onChange={(event) => setAppleAccountName(event.currentTarget.value)}
|
|
102
|
-
/>
|
|
103
|
-
</label>
|
|
104
|
-
<label className="lr-device-install__field">
|
|
105
|
-
<span>Apple ID password</span>
|
|
106
|
-
<input
|
|
107
|
-
type="password"
|
|
108
|
-
autoComplete="current-password"
|
|
109
|
-
placeholder="Password stays in this browser"
|
|
110
|
-
value={applePassword}
|
|
111
|
-
onChange={(event) => setApplePassword(event.currentTarget.value)}
|
|
112
|
-
/>
|
|
113
|
-
</label>
|
|
114
|
-
<label className="lr-device-install__field">
|
|
115
|
-
<span>Generated .p12 password</span>
|
|
116
|
-
<input
|
|
117
|
-
type="password"
|
|
118
|
-
placeholder="Used when exporting Apple certificate"
|
|
119
|
-
onChange={(event) =>
|
|
120
|
-
deviceInstall.setSigningFiles({ certificatePassword: event.currentTarget.value })
|
|
121
|
-
}
|
|
122
|
-
/>
|
|
123
|
-
</label>
|
|
124
|
-
</div>
|
|
125
|
-
<div className="lr-device-install__actions">
|
|
103
|
+
<div className="lr-device-install__choice-grid">
|
|
126
104
|
<button
|
|
127
105
|
type="button"
|
|
128
|
-
className=
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
!applePassword ||
|
|
134
|
-
deviceInstall.busyAction === 'build'
|
|
135
|
-
}
|
|
136
|
-
onClick={() =>
|
|
137
|
-
void deviceInstall.startAppleIDLogin({
|
|
138
|
-
accountName: appleAccountName,
|
|
139
|
-
password: applePassword,
|
|
140
|
-
})
|
|
141
|
-
}
|
|
106
|
+
className={clsx(
|
|
107
|
+
'lr-device-install__choice',
|
|
108
|
+
signingSection === 'apple-id' && 'lr-device-install__choice--active',
|
|
109
|
+
)}
|
|
110
|
+
onClick={() => setSigningSection('apple-id')}
|
|
142
111
|
>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
112
|
+
<strong>Apple ID login</strong>
|
|
113
|
+
<span>Sign in, choose team, bundle ID, and registered devices, then generate signing assets.</span>
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
className={clsx(
|
|
118
|
+
'lr-device-install__choice',
|
|
119
|
+
signingSection === 'upload' && 'lr-device-install__choice--active',
|
|
120
|
+
)}
|
|
121
|
+
onClick={() => setSigningSection('upload')}
|
|
122
|
+
>
|
|
123
|
+
<strong>Upload certificates</strong>
|
|
124
|
+
<span>Use an existing .p12 certificate and provisioning profile.</span>
|
|
146
125
|
</button>
|
|
147
|
-
<span className="lr-device-install__hint">
|
|
148
|
-
Apple password is used only by browser-side SRP. Status: {deviceInstall.appleSigningStatus}
|
|
149
|
-
</span>
|
|
150
126
|
</div>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
127
|
+
|
|
128
|
+
{signingSection === 'apple-id' && (
|
|
129
|
+
<div className="lr-device-install__section-panel">
|
|
130
|
+
<div className="lr-device-install__grid">
|
|
131
|
+
<label className="lr-device-install__field">
|
|
132
|
+
<span>Apple ID</span>
|
|
133
|
+
<input
|
|
134
|
+
type="email"
|
|
135
|
+
autoComplete="username"
|
|
136
|
+
placeholder="name@example.com"
|
|
137
|
+
value={appleAccountName}
|
|
138
|
+
onChange={(event) => setAppleAccountName(event.currentTarget.value)}
|
|
139
|
+
/>
|
|
140
|
+
</label>
|
|
141
|
+
<label className="lr-device-install__field">
|
|
142
|
+
<span>Apple ID password</span>
|
|
143
|
+
<input
|
|
144
|
+
type="password"
|
|
145
|
+
autoComplete="current-password"
|
|
146
|
+
placeholder="Password stays in this browser"
|
|
147
|
+
value={applePassword}
|
|
148
|
+
onChange={(event) => setApplePassword(event.currentTarget.value)}
|
|
149
|
+
/>
|
|
150
|
+
</label>
|
|
151
|
+
{!deviceInstall.hasReusableAppleCertificate && (
|
|
152
|
+
<label className="lr-device-install__field">
|
|
153
|
+
<span>Generated .p12 password</span>
|
|
154
|
+
<input
|
|
155
|
+
type="password"
|
|
156
|
+
placeholder="Used when exporting Apple certificate"
|
|
157
|
+
onChange={(event) =>
|
|
158
|
+
deviceInstall.setSigningFiles({ certificatePassword: event.currentTarget.value })
|
|
159
|
+
}
|
|
160
|
+
/>
|
|
161
|
+
</label>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
<div className="lr-device-install__actions">
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
className="lr-device-install__secondary"
|
|
168
|
+
disabled={
|
|
169
|
+
disabled ||
|
|
170
|
+
!hookOptions.apiUrl ||
|
|
171
|
+
!appleAccountName ||
|
|
172
|
+
!applePassword ||
|
|
173
|
+
deviceInstall.busyAction === 'signing'
|
|
174
|
+
}
|
|
175
|
+
onClick={() =>
|
|
176
|
+
void deviceInstall.startAppleIDLogin({
|
|
177
|
+
accountName: appleAccountName,
|
|
178
|
+
password: applePassword,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
>
|
|
182
|
+
{deviceInstall.appleSigningStatus === 'authenticating'
|
|
183
|
+
? 'Signing in...'
|
|
184
|
+
: 'Sign in with Apple ID'}
|
|
185
|
+
</button>
|
|
186
|
+
<span className="lr-device-install__hint">
|
|
187
|
+
Apple password is used only by browser-side SRP. Status: {deviceInstall.appleSigningStatus}
|
|
188
|
+
</span>
|
|
189
|
+
</div>
|
|
190
|
+
{deviceInstall.appleSigningStatus === 'two-factor-required' && (
|
|
191
|
+
<div className="lr-device-install__grid">
|
|
192
|
+
<label className="lr-device-install__field">
|
|
193
|
+
<span>Two-factor code</span>
|
|
194
|
+
<input
|
|
195
|
+
type="text"
|
|
196
|
+
inputMode="numeric"
|
|
197
|
+
autoComplete="one-time-code"
|
|
198
|
+
value={appleTwoFactorCode}
|
|
199
|
+
onChange={(event) => setAppleTwoFactorCode(event.currentTarget.value)}
|
|
200
|
+
/>
|
|
201
|
+
</label>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
className="lr-device-install__secondary"
|
|
205
|
+
disabled={!appleTwoFactorCode || deviceInstall.busyAction === 'signing'}
|
|
206
|
+
onClick={() => void deviceInstall.submitAppleTwoFactorCode(appleTwoFactorCode)}
|
|
207
|
+
>
|
|
208
|
+
Submit Apple ID code
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
{deviceInstall.appleTeams.length > 0 && (
|
|
213
|
+
<label className="lr-device-install__field">
|
|
214
|
+
<span>Apple Developer team</span>
|
|
215
|
+
<select
|
|
216
|
+
value={deviceInstall.selectedAppleTeamID ?? ''}
|
|
217
|
+
onChange={(event) =>
|
|
218
|
+
deviceInstall.setSelectedAppleTeamID(event.currentTarget.value || undefined)
|
|
219
|
+
}
|
|
220
|
+
>
|
|
221
|
+
{deviceInstall.appleTeams.map((team, index) => {
|
|
222
|
+
const teamID =
|
|
223
|
+
team.teamId ??
|
|
224
|
+
(team.providerId === undefined ? undefined : String(team.providerId)) ??
|
|
225
|
+
team.publicProviderId ??
|
|
226
|
+
'';
|
|
227
|
+
return (
|
|
228
|
+
<option key={`${teamID}-${index}`} value={teamID}>
|
|
229
|
+
{team.name ?? 'Apple Developer Team'} {teamID ? `(${teamID})` : ''}
|
|
230
|
+
</option>
|
|
231
|
+
);
|
|
232
|
+
})}
|
|
233
|
+
</select>
|
|
234
|
+
</label>
|
|
235
|
+
)}
|
|
236
|
+
{deviceInstall.appleDevices.length > 0 && (
|
|
237
|
+
<label className="lr-device-install__field">
|
|
238
|
+
<span>Apple Developer devices</span>
|
|
239
|
+
<select
|
|
240
|
+
multiple
|
|
241
|
+
value={deviceInstall.selectedAppleDeviceIDs}
|
|
242
|
+
onChange={(event) =>
|
|
243
|
+
deviceInstall.setSelectedAppleDeviceIDs(
|
|
244
|
+
Array.from(event.currentTarget.selectedOptions).map((option) => option.value),
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
>
|
|
248
|
+
{deviceInstall.appleDevices.map((appleDevice) => (
|
|
249
|
+
<option
|
|
250
|
+
key={appleDevice.deviceId ?? appleDevice.deviceNumber}
|
|
251
|
+
value={appleDevice.deviceId ?? ''}
|
|
252
|
+
>
|
|
253
|
+
{appleDevice.name ?? appleDevice.model ?? 'Apple device'} {appleDevice.deviceNumber ?? ''}
|
|
254
|
+
</option>
|
|
255
|
+
))}
|
|
256
|
+
</select>
|
|
257
|
+
</label>
|
|
258
|
+
)}
|
|
259
|
+
{deviceInstall.applePortalSummary && (
|
|
260
|
+
<p className="lr-device-install__hint">
|
|
261
|
+
Found {deviceInstall.applePortalSummary.certificateCount} certificates and{' '}
|
|
262
|
+
{deviceInstall.applePortalSummary.profileCount} provisioning profiles.
|
|
263
|
+
</p>
|
|
264
|
+
)}
|
|
265
|
+
{deviceInstall.hasReusableAppleCertificate && (
|
|
266
|
+
<p className="lr-device-install__hint">
|
|
267
|
+
Reusing the certificate and private key stored in this browser.
|
|
268
|
+
</p>
|
|
269
|
+
)}
|
|
163
270
|
<button
|
|
164
271
|
type="button"
|
|
165
|
-
className="lr-device-
|
|
166
|
-
disabled={
|
|
167
|
-
onClick={() => void deviceInstall.
|
|
272
|
+
className="lr-device-install__primary"
|
|
273
|
+
disabled={disabled || !deviceInstall.canPrepareAppleSigningAssets}
|
|
274
|
+
onClick={() => void deviceInstall.prepareAppleSigningAssets()}
|
|
168
275
|
>
|
|
169
|
-
|
|
276
|
+
{deviceInstall.appleSigningStatus === 'preparing-assets'
|
|
277
|
+
? 'Preparing signing assets...'
|
|
278
|
+
: 'Generate certificate and profile'}
|
|
170
279
|
</button>
|
|
171
280
|
</div>
|
|
172
281
|
)}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
282
|
+
|
|
283
|
+
{signingSection === 'upload' && (
|
|
284
|
+
<div className="lr-device-install__section-panel">
|
|
285
|
+
<div className="lr-device-install__grid">
|
|
286
|
+
<label className="lr-device-install__field">
|
|
287
|
+
<span>Certificate (.p12)</span>
|
|
288
|
+
<input
|
|
289
|
+
type="file"
|
|
290
|
+
accept=".p12,application/x-pkcs12"
|
|
291
|
+
onChange={(event) => updateSigningFiles('certificateFile', event)}
|
|
292
|
+
/>
|
|
293
|
+
</label>
|
|
294
|
+
<label className="lr-device-install__field">
|
|
295
|
+
<span>Provisioning profile</span>
|
|
296
|
+
<input
|
|
297
|
+
type="file"
|
|
298
|
+
accept=".mobileprovision"
|
|
299
|
+
onChange={(event) => updateSigningFiles('provisioningProfileFile', event)}
|
|
300
|
+
/>
|
|
301
|
+
</label>
|
|
302
|
+
<label className="lr-device-install__field">
|
|
303
|
+
<span>Uploaded .p12 password</span>
|
|
304
|
+
<input
|
|
305
|
+
type="password"
|
|
306
|
+
placeholder="Export password"
|
|
307
|
+
onChange={(event) =>
|
|
308
|
+
deviceInstall.setSigningFiles({ certificatePassword: event.currentTarget.value })
|
|
309
|
+
}
|
|
310
|
+
/>
|
|
311
|
+
</label>
|
|
312
|
+
</div>
|
|
313
|
+
<p className="lr-device-install__hint">
|
|
314
|
+
The provisioning profile will be checked against the connected iPhone before installation.
|
|
315
|
+
</p>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{deviceInstall.hasSigningAssets && (
|
|
320
|
+
<p>Signing assets are stored in this browser for the selected bundle and device.</p>
|
|
196
321
|
)}
|
|
197
|
-
<div className="lr-device-install__grid">
|
|
198
|
-
<label className="lr-device-install__field">
|
|
199
|
-
<span>Certificate (.p12)</span>
|
|
200
|
-
<input
|
|
201
|
-
type="file"
|
|
202
|
-
accept=".p12,application/x-pkcs12"
|
|
203
|
-
onChange={(event) => updateSigningFiles('certificateFile', event)}
|
|
204
|
-
/>
|
|
205
|
-
</label>
|
|
206
|
-
<label className="lr-device-install__field">
|
|
207
|
-
<span>Provisioning profile</span>
|
|
208
|
-
<input
|
|
209
|
-
type="file"
|
|
210
|
-
accept=".mobileprovision"
|
|
211
|
-
onChange={(event) => updateSigningFiles('provisioningProfileFile', event)}
|
|
212
|
-
/>
|
|
213
|
-
</label>
|
|
214
|
-
<label className="lr-device-install__field">
|
|
215
|
-
<span>Uploaded .p12 password</span>
|
|
216
|
-
<input
|
|
217
|
-
type="password"
|
|
218
|
-
placeholder="Export password"
|
|
219
|
-
onChange={(event) =>
|
|
220
|
-
deviceInstall.setSigningFiles({ certificatePassword: event.currentTarget.value })
|
|
221
|
-
}
|
|
222
|
-
/>
|
|
223
|
-
</label>
|
|
224
|
-
</div>
|
|
225
|
-
<button
|
|
226
|
-
type="button"
|
|
227
|
-
className="lr-device-install__primary"
|
|
228
|
-
disabled={disabled || !deviceInstall.canBuild}
|
|
229
|
-
onClick={() => void deviceInstall.startDeviceBuild()}
|
|
230
|
-
>
|
|
231
|
-
{deviceInstall.busyAction === 'build' ? 'Starting build...' : 'Start device build'}
|
|
232
|
-
</button>
|
|
233
|
-
<details
|
|
234
|
-
className="lr-device-install__build-logs"
|
|
235
|
-
open={deviceInstall.buildLogPanelOpen}
|
|
236
|
-
onToggle={(event) => deviceInstall.setBuildLogPanelOpen(event.currentTarget.open)}
|
|
237
|
-
>
|
|
238
|
-
<summary>Build logs ({deviceInstall.buildStatus})</summary>
|
|
239
|
-
<pre>
|
|
240
|
-
{deviceInstall.buildLogs.length > 0
|
|
241
|
-
? deviceInstall.buildLogs
|
|
242
|
-
.filter((line) => line.type !== 'meta')
|
|
243
|
-
.map((line) => line.data)
|
|
244
|
-
.join('\n')
|
|
245
|
-
: 'Build logs will appear here while the device build is running.'}
|
|
246
|
-
</pre>
|
|
247
|
-
</details>
|
|
248
322
|
</div>
|
|
249
323
|
)}
|
|
250
324
|
|
|
251
|
-
{step.id === '
|
|
325
|
+
{step.id === 'connect' && (
|
|
252
326
|
<div className="lr-device-install__step-body">
|
|
253
327
|
<p>
|
|
254
|
-
WebUSB works in Chromium browsers on secure origins.
|
|
255
|
-
the browser permission prompt.
|
|
328
|
+
WebUSB works in Chromium browsers on secure origins. Once the build succeeds, connect the iPhone
|
|
329
|
+
over USB, approve the browser permission prompt, then pair this browser.
|
|
256
330
|
</p>
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
331
|
+
<div className="lr-device-install__actions">
|
|
332
|
+
<button
|
|
333
|
+
type="button"
|
|
334
|
+
className="lr-device-install__primary"
|
|
335
|
+
disabled={disabled || !deviceInstall.canRequestUSBAccess}
|
|
336
|
+
onClick={() => void deviceInstall.requestUSBAccess()}
|
|
337
|
+
>
|
|
338
|
+
{deviceInstall.busyAction === 'usb' ? 'Selecting iPhone...' : 'Allow USB access'}
|
|
339
|
+
</button>
|
|
340
|
+
<button
|
|
341
|
+
type="button"
|
|
342
|
+
className="lr-device-install__secondary"
|
|
343
|
+
disabled={disabled || !deviceInstall.canPairBrowser}
|
|
344
|
+
onClick={() => void deviceInstall.pairBrowser()}
|
|
345
|
+
>
|
|
346
|
+
{deviceInstall.busyAction === 'pair'
|
|
347
|
+
? 'Pairing...'
|
|
348
|
+
: deviceInstall.pairConfirmationRequired
|
|
349
|
+
? 'Confirm pair record'
|
|
350
|
+
: 'Pair browser'}
|
|
351
|
+
</button>
|
|
352
|
+
</div>
|
|
265
353
|
{deviceInstall.device && (
|
|
266
354
|
<div className="lr-device-install__device">
|
|
267
355
|
{`${deviceInstall.device.productName ?? 'iPhone'} ${
|
|
@@ -269,34 +357,49 @@ export function DeviceInstallDialog({
|
|
|
269
357
|
}`.trim()}
|
|
270
358
|
</div>
|
|
271
359
|
)}
|
|
272
|
-
</div>
|
|
273
|
-
)}
|
|
274
|
-
|
|
275
|
-
{step.id === 'pair' && (
|
|
276
|
-
<div className="lr-device-install__step-body">
|
|
277
360
|
{deviceInstall.pairConfirmationRequired && (
|
|
278
361
|
<p>
|
|
279
362
|
Unlock the iPhone and tap <strong>Trust</strong> in the system dialog, then confirm the pair
|
|
280
363
|
record.
|
|
281
364
|
</p>
|
|
282
365
|
)}
|
|
366
|
+
<p>
|
|
367
|
+
{deviceInstall.hasPairRecord
|
|
368
|
+
? 'Pair record is stored locally. Continue to installation.'
|
|
369
|
+
: 'Pair this browser once before installing.'}
|
|
370
|
+
</p>
|
|
371
|
+
</div>
|
|
372
|
+
)}
|
|
373
|
+
|
|
374
|
+
{step.id === 'build' && (
|
|
375
|
+
<div className="lr-device-install__step-body">
|
|
376
|
+
<div className="lr-device-install__checklist">
|
|
377
|
+
<StatusLine label="Signing assets" ready={deviceInstall.hasSigningInputs} />
|
|
378
|
+
<StatusLine label="Device build" ready={deviceInstall.buildStatus === 'succeeded' ? true : undefined} pendingText="Not started" />
|
|
379
|
+
</div>
|
|
283
380
|
<button
|
|
284
381
|
type="button"
|
|
285
382
|
className="lr-device-install__primary"
|
|
286
|
-
disabled={disabled || !deviceInstall.
|
|
287
|
-
onClick={() => void deviceInstall.
|
|
383
|
+
disabled={disabled || !deviceInstall.canBuild}
|
|
384
|
+
onClick={() => void deviceInstall.startDeviceBuild()}
|
|
288
385
|
>
|
|
289
|
-
{deviceInstall.busyAction === '
|
|
290
|
-
? 'Pairing...'
|
|
291
|
-
: deviceInstall.pairConfirmationRequired
|
|
292
|
-
? 'Confirm pair record'
|
|
293
|
-
: 'Pair browser'}
|
|
386
|
+
{deviceInstall.busyAction === 'build' ? 'Starting build...' : 'Start device build'}
|
|
294
387
|
</button>
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
388
|
+
<details
|
|
389
|
+
className="lr-device-install__build-logs"
|
|
390
|
+
open={deviceInstall.buildLogPanelOpen}
|
|
391
|
+
onToggle={(event) => deviceInstall.setBuildLogPanelOpen(event.currentTarget.open)}
|
|
392
|
+
>
|
|
393
|
+
<summary>Build logs ({deviceInstall.buildStatus})</summary>
|
|
394
|
+
<pre>
|
|
395
|
+
{deviceInstall.buildLogs.length > 0
|
|
396
|
+
? deviceInstall.buildLogs
|
|
397
|
+
.filter((line) => line.type !== 'meta')
|
|
398
|
+
.map((line) => line.data)
|
|
399
|
+
.join('\n')
|
|
400
|
+
: 'Build logs will appear here while the device build is running.'}
|
|
401
|
+
</pre>
|
|
402
|
+
</details>
|
|
300
403
|
</div>
|
|
301
404
|
)}
|
|
302
405
|
|
|
@@ -338,26 +441,55 @@ function StepCard({
|
|
|
338
441
|
index,
|
|
339
442
|
step,
|
|
340
443
|
active,
|
|
444
|
+
open,
|
|
341
445
|
status,
|
|
446
|
+
onToggle,
|
|
342
447
|
children,
|
|
343
448
|
}: {
|
|
344
449
|
index: number;
|
|
345
450
|
step: { id: DeviceInstallStep; title: string; description: string };
|
|
346
451
|
active: boolean;
|
|
452
|
+
open: boolean;
|
|
347
453
|
status: DeviceInstallStepStatus;
|
|
454
|
+
onToggle: () => void;
|
|
348
455
|
children: ReactNode;
|
|
349
456
|
}) {
|
|
350
457
|
return (
|
|
351
458
|
<article className={clsx('lr-device-install__step', active && 'lr-device-install__step--active')}>
|
|
352
|
-
<
|
|
459
|
+
<button
|
|
460
|
+
type="button"
|
|
461
|
+
className="lr-device-install__step-header"
|
|
462
|
+
aria-expanded={open}
|
|
463
|
+
onClick={onToggle}
|
|
464
|
+
>
|
|
353
465
|
<div className="lr-device-install__step-number">{index}</div>
|
|
354
466
|
<div>
|
|
355
467
|
<h3>{step.title}</h3>
|
|
356
468
|
<p>{step.description}</p>
|
|
357
469
|
</div>
|
|
358
|
-
<span className={clsx('lr-device-install__status', `lr-device-install__status--${status}`)}>
|
|
359
|
-
|
|
360
|
-
|
|
470
|
+
<span className={clsx('lr-device-install__status', `lr-device-install__status--${status}`)}>
|
|
471
|
+
{status === 'complete' ? '✓ Completed' : status}
|
|
472
|
+
</span>
|
|
473
|
+
</button>
|
|
474
|
+
{open && children}
|
|
361
475
|
</article>
|
|
362
476
|
);
|
|
363
477
|
}
|
|
478
|
+
|
|
479
|
+
function StatusLine({
|
|
480
|
+
label,
|
|
481
|
+
ready,
|
|
482
|
+
pendingText = 'Not ready',
|
|
483
|
+
}: {
|
|
484
|
+
label: string;
|
|
485
|
+
ready?: boolean;
|
|
486
|
+
pendingText?: string;
|
|
487
|
+
}) {
|
|
488
|
+
const text = ready === undefined ? pendingText : ready ? 'Ready' : 'Needs attention';
|
|
489
|
+
return (
|
|
490
|
+
<div className="lr-device-install__check-row">
|
|
491
|
+
<span>{label}</span>
|
|
492
|
+
<strong>{text}</strong>
|
|
493
|
+
</div>
|
|
494
|
+
);
|
|
495
|
+
}
|