@limrun/ui 0.9.0-rc.2 → 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/storage/browser-storage.d.ts +1 -0
- package/dist/core/device-install/types.d.ts +2 -2
- package/dist/device-install/index.cjs +1 -1
- package/dist/device-install/index.js +45 -44
- 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 +3 -0
- 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-C1uVac59.mjs → use-device-install-j1Gekpl4.mjs} +2866 -2784
- 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 +329 -248
- package/src/core/device-install/storage/browser-storage.ts +12 -0
- package/src/core/device-install/types.ts +2 -2
- package/src/hooks/use-device-install.ts +227 -84
- package/dist/device-install-dialog-CSwQgbBm.js +0 -2
- package/dist/device-install-dialog-nThj775b.mjs +0 -395
- package/dist/use-device-install-Ca4jcVKU.js +0 -31
|
@@ -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,208 +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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
className="lr-device-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
})
|
|
181
180
|
}
|
|
182
181
|
>
|
|
183
|
-
{deviceInstall.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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 ?? ''}
|
|
192
254
|
</option>
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
</
|
|
196
|
-
|
|
255
|
+
))}
|
|
256
|
+
</select>
|
|
257
|
+
</label>
|
|
258
|
+
)}
|
|
197
259
|
{deviceInstall.applePortalSummary && (
|
|
198
260
|
<p className="lr-device-install__hint">
|
|
199
261
|
Found {deviceInstall.applePortalSummary.certificateCount} certificates and{' '}
|
|
200
262
|
{deviceInstall.applePortalSummary.profileCount} provisioning profiles.
|
|
201
263
|
</p>
|
|
202
264
|
)}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
265
|
+
{deviceInstall.hasReusableAppleCertificate && (
|
|
266
|
+
<p className="lr-device-install__hint">
|
|
267
|
+
Reusing the certificate and private key stored in this browser.
|
|
268
|
+
</p>
|
|
269
|
+
)}
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
className="lr-device-install__primary"
|
|
273
|
+
disabled={disabled || !deviceInstall.canPrepareAppleSigningAssets}
|
|
274
|
+
onClick={() => void deviceInstall.prepareAppleSigningAssets()}
|
|
211
275
|
>
|
|
212
|
-
{deviceInstall.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
276
|
+
{deviceInstall.appleSigningStatus === 'preparing-assets'
|
|
277
|
+
? 'Preparing signing assets...'
|
|
278
|
+
: 'Generate certificate and profile'}
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
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>
|
|
222
321
|
)}
|
|
223
|
-
<div className="lr-device-install__grid">
|
|
224
|
-
<label className="lr-device-install__field">
|
|
225
|
-
<span>Certificate (.p12)</span>
|
|
226
|
-
<input
|
|
227
|
-
type="file"
|
|
228
|
-
accept=".p12,application/x-pkcs12"
|
|
229
|
-
onChange={(event) => updateSigningFiles('certificateFile', event)}
|
|
230
|
-
/>
|
|
231
|
-
</label>
|
|
232
|
-
<label className="lr-device-install__field">
|
|
233
|
-
<span>Provisioning profile</span>
|
|
234
|
-
<input
|
|
235
|
-
type="file"
|
|
236
|
-
accept=".mobileprovision"
|
|
237
|
-
onChange={(event) => updateSigningFiles('provisioningProfileFile', event)}
|
|
238
|
-
/>
|
|
239
|
-
</label>
|
|
240
|
-
<label className="lr-device-install__field">
|
|
241
|
-
<span>Uploaded .p12 password</span>
|
|
242
|
-
<input
|
|
243
|
-
type="password"
|
|
244
|
-
placeholder="Export password"
|
|
245
|
-
onChange={(event) =>
|
|
246
|
-
deviceInstall.setSigningFiles({ certificatePassword: event.currentTarget.value })
|
|
247
|
-
}
|
|
248
|
-
/>
|
|
249
|
-
</label>
|
|
250
|
-
</div>
|
|
251
|
-
<button
|
|
252
|
-
type="button"
|
|
253
|
-
className="lr-device-install__primary"
|
|
254
|
-
disabled={disabled || !deviceInstall.canBuild}
|
|
255
|
-
onClick={() => void deviceInstall.startDeviceBuild()}
|
|
256
|
-
>
|
|
257
|
-
{deviceInstall.busyAction === 'build' ? 'Starting build...' : 'Start device build'}
|
|
258
|
-
</button>
|
|
259
|
-
<details
|
|
260
|
-
className="lr-device-install__build-logs"
|
|
261
|
-
open={deviceInstall.buildLogPanelOpen}
|
|
262
|
-
onToggle={(event) => deviceInstall.setBuildLogPanelOpen(event.currentTarget.open)}
|
|
263
|
-
>
|
|
264
|
-
<summary>Build logs ({deviceInstall.buildStatus})</summary>
|
|
265
|
-
<pre>
|
|
266
|
-
{deviceInstall.buildLogs.length > 0
|
|
267
|
-
? deviceInstall.buildLogs
|
|
268
|
-
.filter((line) => line.type !== 'meta')
|
|
269
|
-
.map((line) => line.data)
|
|
270
|
-
.join('\n')
|
|
271
|
-
: 'Build logs will appear here while the device build is running.'}
|
|
272
|
-
</pre>
|
|
273
|
-
</details>
|
|
274
322
|
</div>
|
|
275
323
|
)}
|
|
276
324
|
|
|
277
|
-
{step.id === '
|
|
325
|
+
{step.id === 'connect' && (
|
|
278
326
|
<div className="lr-device-install__step-body">
|
|
279
327
|
<p>
|
|
280
|
-
WebUSB works in Chromium browsers on secure origins. Connect the iPhone over USB
|
|
281
|
-
|
|
328
|
+
WebUSB works in Chromium browsers on secure origins. Connect the iPhone over USB, approve the
|
|
329
|
+
browser permission prompt, then pair this browser.
|
|
282
330
|
</p>
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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>
|
|
291
353
|
{deviceInstall.device && (
|
|
292
354
|
<div className="lr-device-install__device">
|
|
293
355
|
{`${deviceInstall.device.productName ?? 'iPhone'} ${
|
|
@@ -295,27 +357,35 @@ export function DeviceInstallDialog({
|
|
|
295
357
|
}`.trim()}
|
|
296
358
|
</div>
|
|
297
359
|
)}
|
|
298
|
-
{deviceInstall.
|
|
299
|
-
<
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
value={deviceInstall.selectedAppleDeviceIDs}
|
|
304
|
-
onChange={(event) =>
|
|
305
|
-
deviceInstall.setSelectedAppleDeviceIDs(
|
|
306
|
-
Array.from(event.currentTarget.selectedOptions).map((option) => option.value),
|
|
307
|
-
)
|
|
308
|
-
}
|
|
309
|
-
>
|
|
310
|
-
{deviceInstall.appleDevices.map((appleDevice) => (
|
|
311
|
-
<option key={appleDevice.deviceId ?? appleDevice.deviceNumber} value={appleDevice.deviceId ?? ''}>
|
|
312
|
-
{appleDevice.name ?? appleDevice.model ?? 'Apple device'} {appleDevice.deviceNumber ?? ''}
|
|
313
|
-
</option>
|
|
314
|
-
))}
|
|
315
|
-
</select>
|
|
316
|
-
</label>
|
|
360
|
+
{deviceInstall.pairConfirmationRequired && (
|
|
361
|
+
<p>
|
|
362
|
+
Unlock the iPhone and tap <strong>Trust</strong> in the system dialog, then confirm the pair
|
|
363
|
+
record.
|
|
364
|
+
</p>
|
|
317
365
|
)}
|
|
318
|
-
|
|
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 && (
|
|
319
389
|
<button
|
|
320
390
|
type="button"
|
|
321
391
|
className="lr-device-install__secondary"
|
|
@@ -325,47 +395,29 @@ export function DeviceInstallDialog({
|
|
|
325
395
|
Register connected iPhone
|
|
326
396
|
</button>
|
|
327
397
|
)}
|
|
328
|
-
<button
|
|
329
|
-
type="button"
|
|
330
|
-
className="lr-device-install__secondary"
|
|
331
|
-
disabled={disabled || !deviceInstall.canPrepareAppleSigningAssets}
|
|
332
|
-
onClick={() => void deviceInstall.prepareAppleSigningAssets()}
|
|
333
|
-
>
|
|
334
|
-
{deviceInstall.appleSigningStatus === 'preparing-assets'
|
|
335
|
-
? 'Preparing signing assets...'
|
|
336
|
-
: 'Generate certificate and profile'}
|
|
337
|
-
</button>
|
|
338
|
-
{deviceInstall.hasSigningAssets && (
|
|
339
|
-
<p>Signing assets are stored in this browser for the selected bundle and device.</p>
|
|
340
|
-
)}
|
|
341
|
-
</div>
|
|
342
|
-
)}
|
|
343
|
-
|
|
344
|
-
{step.id === 'pair' && (
|
|
345
|
-
<div className="lr-device-install__step-body">
|
|
346
|
-
{deviceInstall.pairConfirmationRequired && (
|
|
347
|
-
<p>
|
|
348
|
-
Unlock the iPhone and tap <strong>Trust</strong> in the system dialog, then confirm the pair
|
|
349
|
-
record.
|
|
350
|
-
</p>
|
|
351
|
-
)}
|
|
352
398
|
<button
|
|
353
399
|
type="button"
|
|
354
400
|
className="lr-device-install__primary"
|
|
355
|
-
disabled={disabled || !deviceInstall.
|
|
356
|
-
onClick={() => void deviceInstall.
|
|
401
|
+
disabled={disabled || !deviceInstall.canBuild}
|
|
402
|
+
onClick={() => void deviceInstall.startDeviceBuild()}
|
|
357
403
|
>
|
|
358
|
-
{deviceInstall.busyAction === '
|
|
359
|
-
? 'Pairing...'
|
|
360
|
-
: deviceInstall.pairConfirmationRequired
|
|
361
|
-
? 'Confirm pair record'
|
|
362
|
-
: 'Pair browser'}
|
|
404
|
+
{deviceInstall.busyAction === 'build' ? 'Starting build...' : 'Start device build'}
|
|
363
405
|
</button>
|
|
364
|
-
<
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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>
|
|
369
421
|
</div>
|
|
370
422
|
)}
|
|
371
423
|
|
|
@@ -407,26 +459,55 @@ function StepCard({
|
|
|
407
459
|
index,
|
|
408
460
|
step,
|
|
409
461
|
active,
|
|
462
|
+
open,
|
|
410
463
|
status,
|
|
464
|
+
onToggle,
|
|
411
465
|
children,
|
|
412
466
|
}: {
|
|
413
467
|
index: number;
|
|
414
468
|
step: { id: DeviceInstallStep; title: string; description: string };
|
|
415
469
|
active: boolean;
|
|
470
|
+
open: boolean;
|
|
416
471
|
status: DeviceInstallStepStatus;
|
|
472
|
+
onToggle: () => void;
|
|
417
473
|
children: ReactNode;
|
|
418
474
|
}) {
|
|
419
475
|
return (
|
|
420
476
|
<article className={clsx('lr-device-install__step', active && 'lr-device-install__step--active')}>
|
|
421
|
-
<
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
className="lr-device-install__step-header"
|
|
480
|
+
aria-expanded={open}
|
|
481
|
+
onClick={onToggle}
|
|
482
|
+
>
|
|
422
483
|
<div className="lr-device-install__step-number">{index}</div>
|
|
423
484
|
<div>
|
|
424
485
|
<h3>{step.title}</h3>
|
|
425
486
|
<p>{step.description}</p>
|
|
426
487
|
</div>
|
|
427
|
-
<span className={clsx('lr-device-install__status', `lr-device-install__status--${status}`)}>
|
|
428
|
-
|
|
429
|
-
|
|
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}
|
|
430
493
|
</article>
|
|
431
494
|
);
|
|
432
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
|
+
}
|