@limrun/ui 0.9.0-rc.14 → 0.9.0-rc.15

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.
Files changed (30) hide show
  1. package/dist/components/remote-control.d.ts +125 -1
  2. package/dist/core/device-install/apple/provisioning.d.ts +55 -4
  3. package/dist/core/device-install/operations/limbuild-client.d.ts +15 -1
  4. package/dist/core/device-install/storage/browser-storage.d.ts +9 -5
  5. package/dist/core/device-install/types.d.ts +4 -1
  6. package/dist/device-install/index.cjs +1 -1
  7. package/dist/device-install/index.js +70 -64
  8. package/dist/device-install/react.cjs +1 -1
  9. package/dist/device-install/react.js +1 -1
  10. package/dist/device-install-dialog-DY35un0b.js +9 -0
  11. package/dist/device-install-dialog-chNLeiiL.mjs +2000 -0
  12. package/dist/device-install-dialog.css +1 -1
  13. package/dist/hooks/use-device-install.d.ts +5 -1
  14. package/dist/index.cjs +1 -1
  15. package/dist/index.js +1286 -1116
  16. package/dist/use-device-install-BIrl0v-k.js +31 -0
  17. package/dist/{use-device-install-sDVvby1V.mjs → use-device-install-LGfEdqyM.mjs} +4388 -4271
  18. package/package.json +4 -2
  19. package/src/components/device-install/device-install-dialog.css +29 -0
  20. package/src/components/device-install/device-install-dialog.tsx +91 -30
  21. package/src/components/remote-control.tsx +535 -5
  22. package/src/core/device-install/apple/provisioning.test.ts +84 -0
  23. package/src/core/device-install/apple/provisioning.ts +91 -7
  24. package/src/core/device-install/operations/limbuild-client.ts +32 -2
  25. package/src/core/device-install/storage/browser-storage.ts +29 -14
  26. package/src/core/device-install/types.ts +5 -1
  27. package/src/hooks/use-device-install.ts +135 -59
  28. package/dist/device-install-dialog-CjH25hnN.js +0 -2
  29. package/dist/device-install-dialog-W5Xv9kWL.mjs +0 -443
  30. package/dist/use-device-install-Y1u6vIBB.js +0 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limrun/ui",
3
- "version": "0.9.0-rc.14",
3
+ "version": "0.9.0-rc.15",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,6 +42,7 @@
42
42
  "license": "MIT",
43
43
  "devDependencies": {
44
44
  "@types/node-forge": "^1.3.14",
45
+ "@types/qrcode": "^1.5.6",
45
46
  "@types/react": "^19.1.12",
46
47
  "@types/react-dom": "^19.1.9",
47
48
  "@vitejs/plugin-react-swc": "^4.0.1",
@@ -57,6 +58,7 @@
57
58
  "dependencies": {
58
59
  "@foxt/js-srp": "^0.0.3-patch2",
59
60
  "clsx": "^2.1.1",
60
- "node-forge": "^1.4.0"
61
+ "node-forge": "^1.4.0",
62
+ "qrcode": "^1.5.4"
61
63
  }
62
64
  }
@@ -197,6 +197,10 @@
197
197
  padding: 12px;
198
198
  }
199
199
 
200
+ .lr-device-install__section-panel h4 {
201
+ margin: 0 0 4px;
202
+ }
203
+
200
204
  .lr-device-install__grid {
201
205
  display: grid;
202
206
  gap: 10px;
@@ -234,6 +238,31 @@
234
238
  font-size: 13px;
235
239
  }
236
240
 
241
+ .lr-device-install__ota-panel {
242
+ justify-items: start;
243
+ }
244
+
245
+ .lr-device-install__qr {
246
+ border: 1px solid #e5e7eb;
247
+ border-radius: 12px;
248
+ height: 240px;
249
+ width: 240px;
250
+ }
251
+
252
+ .lr-device-install__qr-placeholder {
253
+ align-items: center;
254
+ background: #f9fafb;
255
+ border: 1px dashed #d1d5db;
256
+ border-radius: 12px;
257
+ color: #6b7280;
258
+ display: flex;
259
+ height: 240px;
260
+ justify-content: center;
261
+ padding: 16px;
262
+ text-align: center;
263
+ width: 240px;
264
+ }
265
+
237
266
  .lr-device-install__checklist {
238
267
  border: 1px solid #e5e7eb;
239
268
  border-radius: 10px;
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useId, useState, type ChangeEvent, type ReactNode } from 'react';
2
2
  import { clsx } from 'clsx';
3
+ import QRCode from 'qrcode';
3
4
  import { useDeviceInstall, type UseDeviceInstallOptions } from '../../hooks/use-device-install';
4
5
  import type { DeviceInstallStep, DeviceInstallStepStatus } from '../../core/device-install';
5
6
  import './device-install-dialog.css';
@@ -17,7 +18,7 @@ const steps: Array<{ id: DeviceInstallStep; title: string; description: string }
17
18
  {
18
19
  id: 'build',
19
20
  title: 'Build for device',
20
- description: 'Start the signed iPhone build before connecting over USB.',
21
+ description: 'Start the signed iPhone build for the selected signing mode.',
21
22
  },
22
23
  {
23
24
  id: 'connect',
@@ -27,7 +28,7 @@ const steps: Array<{ id: DeviceInstallStep; title: string; description: string }
27
28
  {
28
29
  id: 'install',
29
30
  title: 'Start installation',
30
- description: 'Relay the last successful device build to the paired iPhone.',
31
+ description: 'Install the last successful device build.',
31
32
  },
32
33
  ];
33
34
 
@@ -43,13 +44,31 @@ export function DeviceInstallDialog({
43
44
  const [appleAccountName, setAppleAccountName] = useState('');
44
45
  const [applePassword, setApplePassword] = useState('');
45
46
  const [appleTwoFactorCode, setAppleTwoFactorCode] = useState('');
47
+ const [otaQRCode, setOTAQRCode] = useState<string>();
46
48
  const dialogTitleId = useId();
47
49
  const deviceInstall = useDeviceInstall(hookOptions);
50
+ const visibleSteps = steps.filter((step) => deviceInstall.signingMode === 'development' || step.id !== 'connect');
48
51
 
49
52
  useEffect(() => {
50
53
  setOpenStep(deviceInstall.currentStep);
51
54
  }, [deviceInstall.currentStep]);
52
55
 
56
+ useEffect(() => {
57
+ let cancelled = false;
58
+ if (!deviceInstall.otaInstall?.landingUrl) {
59
+ setOTAQRCode(undefined);
60
+ return;
61
+ }
62
+ void QRCode.toDataURL(deviceInstall.otaInstall.landingUrl, { margin: 1, width: 240 }).then((dataUrl) => {
63
+ if (!cancelled) {
64
+ setOTAQRCode(dataUrl);
65
+ }
66
+ });
67
+ return () => {
68
+ cancelled = true;
69
+ };
70
+ }, [deviceInstall.otaInstall?.landingUrl]);
71
+
53
72
  const updateSigningFiles = (field: 'certificateFile' | 'provisioningProfileFile', event: ChangeEvent<HTMLInputElement>) => {
54
73
  deviceInstall.setSigningFiles({
55
74
  [field]: event.currentTarget.files?.[0],
@@ -78,7 +97,7 @@ export function DeviceInstallDialog({
78
97
  <header className="lr-device-install__header">
79
98
  <div>
80
99
  <h2 id={dialogTitleId}>Install to a real iPhone</h2>
81
- <p>Prepare signing, build for the registered device, connect and pair, then install from this browser.</p>
100
+ <p>Prepare signing, build for the registered device, then install with QR or WebUSB based on signing mode.</p>
82
101
  </div>
83
102
  <button type="button" className="lr-device-install__icon-button" onClick={() => setOpen(false)}>
84
103
  Close
@@ -88,7 +107,7 @@ export function DeviceInstallDialog({
88
107
  {deviceInstall.error && <div className="lr-device-install__error">{deviceInstall.error}</div>}
89
108
 
90
109
  <div className="lr-device-install__steps">
91
- {steps.map((step, index) => (
110
+ {visibleSteps.map((step, index) => (
92
111
  <StepCard
93
112
  key={step.id}
94
113
  index={index + 1}
@@ -100,6 +119,31 @@ export function DeviceInstallDialog({
100
119
  >
101
120
  {step.id === 'signing' && (
102
121
  <div className="lr-device-install__step-body">
122
+ <div className="lr-device-install__choice-grid">
123
+ <button
124
+ type="button"
125
+ className={clsx(
126
+ 'lr-device-install__choice',
127
+ deviceInstall.signingMode === 'development' && 'lr-device-install__choice--active',
128
+ )}
129
+ onClick={() => deviceInstall.setSigningMode('development')}
130
+ >
131
+ <strong>Development + WebUSB</strong>
132
+ <span>Use Apple Development signing and install by pairing the iPhone with this browser.</span>
133
+ </button>
134
+ <button
135
+ type="button"
136
+ className={clsx(
137
+ 'lr-device-install__choice',
138
+ deviceInstall.signingMode === 'adhoc' && 'lr-device-install__choice--active',
139
+ )}
140
+ onClick={() => deviceInstall.setSigningMode('adhoc')}
141
+ >
142
+ <strong>Ad Hoc + QR</strong>
143
+ <span>Use registered-device Ad Hoc signing and install from iPhone Camera or Safari.</span>
144
+ </button>
145
+ </div>
146
+
103
147
  <div className="lr-device-install__choice-grid">
104
148
  <button
105
149
  type="button"
@@ -148,18 +192,6 @@ export function DeviceInstallDialog({
148
192
  onChange={(event) => setApplePassword(event.currentTarget.value)}
149
193
  />
150
194
  </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
195
  </div>
164
196
  <div className="lr-device-install__actions">
165
197
  <button
@@ -275,7 +307,9 @@ export function DeviceInstallDialog({
275
307
  >
276
308
  {deviceInstall.appleSigningStatus === 'preparing-assets'
277
309
  ? 'Preparing signing assets...'
278
- : 'Generate certificate and profile'}
310
+ : deviceInstall.signingMode === 'adhoc'
311
+ ? 'Generate Ad Hoc signing assets'
312
+ : 'Generate development signing assets'}
279
313
  </button>
280
314
  </div>
281
315
  )}
@@ -300,10 +334,10 @@ export function DeviceInstallDialog({
300
334
  />
301
335
  </label>
302
336
  <label className="lr-device-install__field">
303
- <span>Uploaded .p12 password</span>
337
+ <span>Uploaded .p12 password (optional)</span>
304
338
  <input
305
339
  type="password"
306
- placeholder="Export password"
340
+ placeholder="Leave empty if the certificate has no password"
307
341
  onChange={(event) =>
308
342
  deviceInstall.setSigningFiles({ certificatePassword: event.currentTarget.value })
309
343
  }
@@ -405,17 +439,44 @@ export function DeviceInstallDialog({
405
439
 
406
440
  {step.id === 'install' && (
407
441
  <div className="lr-device-install__step-body">
408
- <button
409
- type="button"
410
- className="lr-device-install__primary"
411
- disabled={disabled || !deviceInstall.canInstall}
412
- onClick={() => void deviceInstall.startInstallation()}
413
- >
414
- {deviceInstall.busyAction === 'install' ? 'Installing...' : 'Install last build'}
415
- </button>
416
- <button type="button" className="lr-device-install__secondary" onClick={deviceInstall.stopRelay}>
417
- Stop relay
418
- </button>
442
+ {deviceInstall.signingMode === 'adhoc' ? (
443
+ <div className="lr-device-install__section-panel lr-device-install__ota-panel">
444
+ <div>
445
+ <h4>Install with iPhone Camera</h4>
446
+ <p className="lr-device-install__hint">
447
+ Scan this QR code on a registered iPhone to open an HTTPS install page in Safari.
448
+ </p>
449
+ </div>
450
+ {otaQRCode ? (
451
+ <img className="lr-device-install__qr" src={otaQRCode} alt="Ad Hoc install QR code" />
452
+ ) : (
453
+ <div className="lr-device-install__qr-placeholder">Build an Ad Hoc IPA to generate a QR code.</div>
454
+ )}
455
+ {deviceInstall.otaInstall && (
456
+ <label className="lr-device-install__field">
457
+ <span>Install page link</span>
458
+ <input readOnly value={deviceInstall.otaInstall.landingUrl} />
459
+ </label>
460
+ )}
461
+ <p className="lr-device-install__hint">
462
+ Requires HTTPS and an Ad Hoc profile containing this device UDID.
463
+ </p>
464
+ </div>
465
+ ) : (
466
+ <>
467
+ <button
468
+ type="button"
469
+ className="lr-device-install__primary"
470
+ disabled={disabled || !deviceInstall.canInstall}
471
+ onClick={() => void deviceInstall.startInstallation()}
472
+ >
473
+ {deviceInstall.busyAction === 'install' ? 'Installing...' : 'Install last build'}
474
+ </button>
475
+ <button type="button" className="lr-device-install__secondary" onClick={deviceInstall.stopRelay}>
476
+ Stop relay
477
+ </button>
478
+ </>
479
+ )}
419
480
  </div>
420
481
  )}
421
482
  </StepCard>