@limrun/ui 0.9.0-rc.1 → 0.9.0-rc.4
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/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-86RDdoK9.js +2 -0
- package/dist/device-install-dialog-CnyDWf0q.mjs +462 -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.js +3 -3
- package/dist/use-device-install-CbGVvwPp.js +31 -0
- package/dist/use-device-install-j1Gekpl4.mjs +13623 -0
- package/package.json +1 -1
- package/src/components/device-install/device-install-dialog.css +82 -1
- package/src/components/device-install/device-install-dialog.tsx +337 -187
- 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/storage/browser-storage.ts +26 -1
- package/src/core/device-install/types.ts +2 -2
- package/src/hooks/use-device-install.ts +748 -60
- 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, then confirm the target developer device.',
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
|
-
id: '
|
|
19
|
-
title: '
|
|
20
|
-
description: '
|
|
18
|
+
id: 'connect',
|
|
19
|
+
title: 'Connect and pair',
|
|
20
|
+
description: 'Connect the iPhone with WebUSB, then pair this browser so installs can use the device.',
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
|
-
id: '
|
|
24
|
-
title: '
|
|
25
|
-
description: '
|
|
23
|
+
id: 'build',
|
|
24
|
+
title: 'Check and build',
|
|
25
|
+
description: 'Verify the device and provisioning profile are ready, then start the signed build.',
|
|
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, connect and pair the device, build, 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, 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 the build starts.
|
|
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. Connect the iPhone over USB
|
|
255
|
-
|
|
328
|
+
WebUSB works in Chromium browsers on secure origins. Connect the iPhone over USB, approve the
|
|
329
|
+
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,67 @@ 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 the build check.'
|
|
369
|
+
: 'Pair this browser once before building and 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} pendingText="Ready to verify" />
|
|
378
|
+
<StatusLine label="USB device" ready={!!deviceInstall.device} />
|
|
379
|
+
<StatusLine label="Pair record" ready={deviceInstall.hasPairRecord} />
|
|
380
|
+
<StatusLine
|
|
381
|
+
label="Profile includes connected device"
|
|
382
|
+
ready={deviceInstall.connectedDeviceInProfile}
|
|
383
|
+
pendingText="Checked when the build starts"
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
{deviceInstall.device &&
|
|
387
|
+
deviceInstall.appleTeams.length > 0 &&
|
|
388
|
+
!deviceInstall.connectedAppleDeviceRegistered && (
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
className="lr-device-install__secondary"
|
|
392
|
+
disabled={disabled || !!deviceInstall.busyAction}
|
|
393
|
+
onClick={() => void deviceInstall.registerConnectedAppleDevice()}
|
|
394
|
+
>
|
|
395
|
+
Register connected iPhone
|
|
396
|
+
</button>
|
|
397
|
+
)}
|
|
283
398
|
<button
|
|
284
399
|
type="button"
|
|
285
400
|
className="lr-device-install__primary"
|
|
286
|
-
disabled={disabled || !deviceInstall.
|
|
287
|
-
onClick={() => void deviceInstall.
|
|
401
|
+
disabled={disabled || !deviceInstall.canBuild}
|
|
402
|
+
onClick={() => void deviceInstall.startDeviceBuild()}
|
|
288
403
|
>
|
|
289
|
-
{deviceInstall.busyAction === '
|
|
290
|
-
? 'Pairing...'
|
|
291
|
-
: deviceInstall.pairConfirmationRequired
|
|
292
|
-
? 'Confirm pair record'
|
|
293
|
-
: 'Pair browser'}
|
|
404
|
+
{deviceInstall.busyAction === 'build' ? 'Starting build...' : 'Start device build'}
|
|
294
405
|
</button>
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
406
|
+
<details
|
|
407
|
+
className="lr-device-install__build-logs"
|
|
408
|
+
open={deviceInstall.buildLogPanelOpen}
|
|
409
|
+
onToggle={(event) => deviceInstall.setBuildLogPanelOpen(event.currentTarget.open)}
|
|
410
|
+
>
|
|
411
|
+
<summary>Build logs ({deviceInstall.buildStatus})</summary>
|
|
412
|
+
<pre>
|
|
413
|
+
{deviceInstall.buildLogs.length > 0
|
|
414
|
+
? deviceInstall.buildLogs
|
|
415
|
+
.filter((line) => line.type !== 'meta')
|
|
416
|
+
.map((line) => line.data)
|
|
417
|
+
.join('\n')
|
|
418
|
+
: 'Build logs will appear here while the device build is running.'}
|
|
419
|
+
</pre>
|
|
420
|
+
</details>
|
|
300
421
|
</div>
|
|
301
422
|
)}
|
|
302
423
|
|
|
@@ -338,26 +459,55 @@ function StepCard({
|
|
|
338
459
|
index,
|
|
339
460
|
step,
|
|
340
461
|
active,
|
|
462
|
+
open,
|
|
341
463
|
status,
|
|
464
|
+
onToggle,
|
|
342
465
|
children,
|
|
343
466
|
}: {
|
|
344
467
|
index: number;
|
|
345
468
|
step: { id: DeviceInstallStep; title: string; description: string };
|
|
346
469
|
active: boolean;
|
|
470
|
+
open: boolean;
|
|
347
471
|
status: DeviceInstallStepStatus;
|
|
472
|
+
onToggle: () => void;
|
|
348
473
|
children: ReactNode;
|
|
349
474
|
}) {
|
|
350
475
|
return (
|
|
351
476
|
<article className={clsx('lr-device-install__step', active && 'lr-device-install__step--active')}>
|
|
352
|
-
<
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
className="lr-device-install__step-header"
|
|
480
|
+
aria-expanded={open}
|
|
481
|
+
onClick={onToggle}
|
|
482
|
+
>
|
|
353
483
|
<div className="lr-device-install__step-number">{index}</div>
|
|
354
484
|
<div>
|
|
355
485
|
<h3>{step.title}</h3>
|
|
356
486
|
<p>{step.description}</p>
|
|
357
487
|
</div>
|
|
358
|
-
<span className={clsx('lr-device-install__status', `lr-device-install__status--${status}`)}>
|
|
359
|
-
|
|
360
|
-
|
|
488
|
+
<span className={clsx('lr-device-install__status', `lr-device-install__status--${status}`)}>
|
|
489
|
+
{status === 'complete' ? '✓ Completed' : status}
|
|
490
|
+
</span>
|
|
491
|
+
</button>
|
|
492
|
+
{open && children}
|
|
361
493
|
</article>
|
|
362
494
|
);
|
|
363
495
|
}
|
|
496
|
+
|
|
497
|
+
function StatusLine({
|
|
498
|
+
label,
|
|
499
|
+
ready,
|
|
500
|
+
pendingText = 'Not ready',
|
|
501
|
+
}: {
|
|
502
|
+
label: string;
|
|
503
|
+
ready?: boolean;
|
|
504
|
+
pendingText?: string;
|
|
505
|
+
}) {
|
|
506
|
+
const text = ready === undefined ? pendingText : ready ? 'Ready' : 'Needs attention';
|
|
507
|
+
return (
|
|
508
|
+
<div className="lr-device-install__check-row">
|
|
509
|
+
<span>{label}</span>
|
|
510
|
+
<strong>{text}</strong>
|
|
511
|
+
</div>
|
|
512
|
+
);
|
|
513
|
+
}
|