@limrun/ui 0.13.3-rc.1 → 0.13.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/app-store-relay/index.cjs +4 -4
- package/dist/app-store-relay/index.js +175 -119
- package/dist/app-store-relay/react.cjs +1 -1
- package/dist/app-store-relay/react.js +2 -2
- package/dist/{browser-storage-BJ__DGJ6.mjs → browser-storage-Cx9eW-bm.mjs} +48 -45
- package/dist/browser-storage-jJaIht4K.js +1 -0
- package/dist/{client-CnbCWvCs.js → client-ASLtHFLh.js} +1 -1
- package/dist/{client-Bk1N3XIF.mjs → client-CGdh8yoJ.mjs} +33 -27
- package/dist/components/remote-control.d.ts +144 -0
- package/dist/core/device-install/apple/provisioning.d.ts +4 -4
- package/dist/device-build/index.cjs +1 -1
- package/dist/device-build/index.js +5 -5
- package/dist/device-build/react.cjs +1 -1
- package/dist/device-build/react.js +15 -13
- package/dist/device-install/demo/demo.d.ts +0 -0
- package/dist/device-install/index.cjs +1 -1
- package/dist/device-install/index.d.ts +1 -1
- package/dist/device-install/index.js +134 -121
- package/dist/device-install/react.cjs +1 -1
- package/dist/device-install/react.js +55 -50
- package/dist/{index-BXg7HdIs.mjs → index-DFdCvi2_.mjs} +3 -7
- package/dist/index.cjs +1 -1
- package/dist/index.js +1336 -1089
- package/dist/limbuild-client-CgEncrgx.js +1 -0
- package/dist/{limbuild-client-CFJhYsRx.mjs → limbuild-client-D7U7w342.mjs} +4 -2
- package/dist/{provisioning-CdseoMJQ.mjs → provisioning-DKbkBO-p.mjs} +11 -11
- package/dist/{provisioning-D2ZZQeyX.js → provisioning-Va0PH4Kk.js} +1 -1
- package/package.json +1 -1
- package/src/app-store-relay/index.test.ts +1 -5
- package/src/app-store-relay/index.ts +109 -42
- package/src/app-store-relay/react.ts +6 -2
- package/src/components/remote-control.tsx +726 -0
- package/src/core/device-install/apple/client.ts +29 -22
- package/src/core/device-install/apple/crypto.ts +9 -6
- package/src/core/device-install/apple/gsa-srp.ts +3 -7
- package/src/core/device-install/apple/provisioning.ts +16 -18
- package/src/core/device-install/apple/relay.ts +5 -1
- package/src/core/device-install/operations/limbuild-client.ts +8 -1
- package/src/core/device-install/operations/operations.ts +3 -1
- package/src/core/device-install/operations/relay-client.ts +57 -27
- package/src/core/device-install/operations/usbmux.ts +3 -3
- package/src/core/device-install/operations/webusb.ts +9 -4
- package/src/core/device-install/storage/browser-storage.ts +20 -6
- package/src/core/device-install/types.ts +1 -7
- package/src/device-build/react.ts +4 -1
- package/src/device-install/README.md +423 -0
- package/src/device-install/demo/demo.css +493 -0
- package/src/device-install/demo/demo.tsx +1069 -0
- package/src/device-install/demo/index.html +12 -0
- package/src/device-install/index.ts +7 -7
- package/src/device-install/react.ts +26 -17
- package/src/index.ts +1 -1
- package/dist/browser-storage-C1jQLXat.js +0 -1
- package/dist/limbuild-client-C_CMNLYV.js +0 -1
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# Install to a real iPhone over WebUSB
|
|
2
|
+
|
|
3
|
+
Build a signed iOS app on Limrun and install it onto a physical iPhone that's
|
|
4
|
+
plugged into the user's own computer — straight from the browser, no Mac and no
|
|
5
|
+
Xcode on their machine. The browser talks to the iPhone over WebUSB; Limrun runs
|
|
6
|
+
the native pairing and install (`libimobiledevice`) on its side and relays the
|
|
7
|
+
USB traffic over a WebSocket.
|
|
8
|
+
|
|
9
|
+
`@limrun/ui` ships the browser primitives and React hooks; your app owns the UI.
|
|
10
|
+
There is no prebuilt wizard, so you decide how much of the Apple Developer flow
|
|
11
|
+
to expose and how the steps look in your product.
|
|
12
|
+
|
|
13
|
+
The flow:
|
|
14
|
+
|
|
15
|
+
1. **Provision** an Xcode build sandbox and get its `apiUrl` + `token`.
|
|
16
|
+
2. **Pair** the iPhone over WebUSB (the user taps _Trust_ once).
|
|
17
|
+
3. **Get signing assets** — upload a `.p12` + `.mobileprovision`, or sign in with
|
|
18
|
+
an Apple ID and let Limrun fetch them.
|
|
19
|
+
4. **Build** a signed `iphoneos` IPA on the sandbox; logs stream back.
|
|
20
|
+
5. **Install** the IPA onto the paired iPhone over the WebUSB relay.
|
|
21
|
+
|
|
22
|
+
> **Want to see it working first?** A complete, runnable reference app — with the
|
|
23
|
+
> upload and Apple Developer signing flows, a status stepper, and error handling —
|
|
24
|
+
> lives in [`src/device-install/demo`](./demo). Read its
|
|
25
|
+
> [`demo.tsx`](./demo/demo.tsx) end to end to see how every hook and helper in this
|
|
26
|
+
> guide fits together, or run it locally (see [Run the demo](#run-the-demo)).
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- A **Chromium** browser (Chrome or Edge). WebUSB is not available in Safari or
|
|
31
|
+
Firefox.
|
|
32
|
+
- A **secure context** — your app must be served over `https://` or `localhost`.
|
|
33
|
+
- A physical iPhone connected over USB; the user unlocks it and taps **Trust**
|
|
34
|
+
during pairing.
|
|
35
|
+
- An Apple signing identity: a development `.p12` (with its private key) and a
|
|
36
|
+
`.mobileprovision` that covers the app's bundle ID **and** the target device's
|
|
37
|
+
UDID. See [Apple signing](#get-signing-assets).
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @limrun/ui
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The feature is split across three subpath entry points so you only pull in what
|
|
46
|
+
you use:
|
|
47
|
+
|
|
48
|
+
| Import | Provides |
|
|
49
|
+
| --------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| `@limrun/ui/device-install` + `/react` | WebUSB device selection, pairing, install relay, pair-record storage (`useDeviceInstallRelay`). |
|
|
51
|
+
| `@limrun/ui/device-build` + `/react` | Signing-asset import/validation, signed-build trigger, log streaming (`useDeviceBuild`). |
|
|
52
|
+
| `@limrun/ui/app-store-relay` + `/react` | Apple ID login (SRP + 2FA) and Apple Developer Portal calls (`useAppleIDLogin`, `list*`/`create*`). |
|
|
53
|
+
|
|
54
|
+
## Get a build sandbox
|
|
55
|
+
|
|
56
|
+
Every call below takes an `apiUrl` and `token` that point at a Limrun Xcode
|
|
57
|
+
sandbox. Provision one with the `@limrun/api` SDK or the CLI, then read them off
|
|
58
|
+
the instance status — see [Build with remote Xcode](https://docs.limrun.com/docs/ios/build-with-xcode).
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import Limrun from '@limrun/api';
|
|
62
|
+
|
|
63
|
+
const lim = new Limrun({ apiKey: process.env['LIM_API_KEY'] });
|
|
64
|
+
const xcode = await lim.xcodeInstances.create({ wait: true, reuseIfExists: true });
|
|
65
|
+
|
|
66
|
+
const apiUrl = xcode.status.apiUrl; // HTTP base for the sandbox
|
|
67
|
+
const token = xcode.status.token; // per-instance bearer
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The sandbox must have your project **synced** before a build runs (`lim xcode build .`
|
|
71
|
+
or `lim xcode sync .`). A build against an empty sandbox returns
|
|
72
|
+
`no synced folder found; call /sync first`.
|
|
73
|
+
|
|
74
|
+
The `token` is an **instance-scoped** bearer, not your org API key. It's safe to hand to the browser for this one sandbox; it can't touch the rest of your account.
|
|
75
|
+
|
|
76
|
+
## Pair the iPhone
|
|
77
|
+
|
|
78
|
+
`useDeviceInstallRelay` drives the WebUSB side. `requestUSBAccess` opens the
|
|
79
|
+
browser's device picker; `pairBrowser` runs the pairing handshake through the
|
|
80
|
+
relay and stores the resulting pair record in the browser's IndexedDB, so the
|
|
81
|
+
user only taps **Trust** once per device.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { useDeviceInstallRelay } from '@limrun/ui/device-install/react';
|
|
85
|
+
|
|
86
|
+
const install = useDeviceInstallRelay({ apiUrl, token, log: (m, d) => console.log(m, d) });
|
|
87
|
+
|
|
88
|
+
// 1. Pick the iPhone (shows Chrome's WebUSB chooser).
|
|
89
|
+
await install.requestUSBAccess();
|
|
90
|
+
// install.device?.hello.serialNumber is the UDID.
|
|
91
|
+
|
|
92
|
+
// 2. Pair (user unlocks + taps Trust). Stored for next time.
|
|
93
|
+
await install.pairBrowser();
|
|
94
|
+
// install.hasPairRecord === true
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
| Field / method | Use |
|
|
98
|
+
| ------------------------------------------ | ------------------------------------------------------------------ |
|
|
99
|
+
| `requestUSBAccess()` | Open the WebUSB picker and select the iPhone. |
|
|
100
|
+
| `pairBrowser()` | Pair through the relay; persists the pair record. |
|
|
101
|
+
| `startInstallation()` | Install the latest signed build onto the paired device. |
|
|
102
|
+
| `device` | Selected device; `device.hello.serialNumber` is the UDID. |
|
|
103
|
+
| `hasPairRecord` / `canPair` / `canInstall` | Gating flags for your buttons. |
|
|
104
|
+
| `busyAction` | `'usb'`, `'pair'`, or `'install'` while an operation is in flight. |
|
|
105
|
+
| `error` | Last error message, if any. |
|
|
106
|
+
|
|
107
|
+
Pairing is independent of the build — you can pair before signing or building.
|
|
108
|
+
Only `startInstallation()` needs both a stored pair record and a successful
|
|
109
|
+
build.
|
|
110
|
+
|
|
111
|
+
## Get signing assets
|
|
112
|
+
|
|
113
|
+
A real-device install must be signed. You produce a `StoredSigningAssets` object
|
|
114
|
+
(persisted in IndexedDB) one of two ways, then hand it to the build step.
|
|
115
|
+
|
|
116
|
+
### Option A — upload files (simplest)
|
|
117
|
+
|
|
118
|
+
Ask the user for a `.p12` (with its private key), its password, and a
|
|
119
|
+
`.mobileprovision`. `importSigningAssetsFromFiles` validates the profile against
|
|
120
|
+
the bundle ID and device, then stores it.
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { importSigningAssetsFromFiles } from '@limrun/ui/device-build';
|
|
124
|
+
|
|
125
|
+
const assets = await importSigningAssetsFromFiles({
|
|
126
|
+
certificateFile, // File (.p12)
|
|
127
|
+
provisioningProfileFile, // File (.mobileprovision)
|
|
128
|
+
certificatePassword,
|
|
129
|
+
bundleId, // e.g. 'com.example.MyApp'
|
|
130
|
+
deviceUDID: install.device?.hello.serialNumber,
|
|
131
|
+
signingMode: 'development',
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Option B — sign in with Apple ID
|
|
136
|
+
|
|
137
|
+
For a guided experience with no files to find, use the App Store Connect relay.
|
|
138
|
+
The user's password never leaves the browser: the SRP proof is computed
|
|
139
|
+
client-side and the relay only forwards Apple's auth and provisioning requests.
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import { useAppleIDLogin } from '@limrun/ui/app-store-relay/react';
|
|
143
|
+
import {
|
|
144
|
+
registerAppleDevice,
|
|
145
|
+
listAppleCertificates,
|
|
146
|
+
createAppleCertificate,
|
|
147
|
+
downloadAppleCertificate,
|
|
148
|
+
exportAppleCertificateP12,
|
|
149
|
+
generateAppleSigningKeyAndCSR,
|
|
150
|
+
createAppleProfile,
|
|
151
|
+
downloadAppleProfile,
|
|
152
|
+
} from '@limrun/ui/app-store-relay';
|
|
153
|
+
import {
|
|
154
|
+
getLatestSigningAssetsWithCertificate,
|
|
155
|
+
parseProvisioningProfileBase64,
|
|
156
|
+
putAppleGeneratedSigningAssets,
|
|
157
|
+
} from '@limrun/ui/device-build';
|
|
158
|
+
|
|
159
|
+
const appleLogin = useAppleIDLogin({ limbuildApiUrl: apiUrl, token });
|
|
160
|
+
const session = await appleLogin.startLogin({ accountName, password });
|
|
161
|
+
if (session?.requiresTwoFactor) await appleLogin.submitTwoFactorCode(code);
|
|
162
|
+
|
|
163
|
+
// Resolve the team id across all three fields Apple may use.
|
|
164
|
+
const teamId = team.teamId ?? String(team.providerId ?? team.publicProviderId ?? '');
|
|
165
|
+
const base = { apiUrl, token, appleSessionId: appleLogin.session!.appleSessionId, teamId };
|
|
166
|
+
|
|
167
|
+
// Register the paired iPhone (no-op if already registered).
|
|
168
|
+
await registerAppleDevice({ ...base, deviceUDID, name: 'My iPhone' });
|
|
169
|
+
|
|
170
|
+
// Reuse a stored cert if its key is in this browser and it's still current;
|
|
171
|
+
// otherwise mint a new one.
|
|
172
|
+
let certificateId: string | undefined;
|
|
173
|
+
let certificateP12Base64: string | undefined;
|
|
174
|
+
const stored = await getLatestSigningAssetsWithCertificate(teamId, 'development');
|
|
175
|
+
if (stored?.certificateID && stored.certificateP12Base64) {
|
|
176
|
+
const current = await listAppleCertificates({ ...base, certificateKind: 'development' });
|
|
177
|
+
const match = current.find(
|
|
178
|
+
(c) => c.certificateId === stored.certificateID || c.certRequestId === stored.certificateID,
|
|
179
|
+
);
|
|
180
|
+
if (match) {
|
|
181
|
+
certificateId = (match.certificateId as string) ?? stored.certificateID; // canonical id
|
|
182
|
+
certificateP12Base64 = stored.certificateP12Base64;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!certificateId) {
|
|
186
|
+
const key = await generateAppleSigningKeyAndCSR({ commonName: 'My App' });
|
|
187
|
+
const created = await createAppleCertificate({
|
|
188
|
+
...base,
|
|
189
|
+
certificateKind: 'development',
|
|
190
|
+
csrPEM: key.csrPEM,
|
|
191
|
+
});
|
|
192
|
+
certificateId = (created?.certificateId as string) ?? (created?.certRequestId as string);
|
|
193
|
+
const cer = await downloadAppleCertificate({ ...base, certificateKind: 'development', certificateId });
|
|
194
|
+
certificateP12Base64 = exportAppleCertificateP12({
|
|
195
|
+
privateKeyPKCS8Base64: key.privateKeyPKCS8Base64,
|
|
196
|
+
certificateBase64: cer.rawBodyBase64!,
|
|
197
|
+
password: certificatePassword,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Create a profile covering the bundle ID + device (unique name avoids
|
|
202
|
+
// "multiple profiles with the same name"), download it, and store everything.
|
|
203
|
+
const profile = await createAppleProfile({
|
|
204
|
+
...base,
|
|
205
|
+
profileKind: 'development',
|
|
206
|
+
bundleId,
|
|
207
|
+
appIdId,
|
|
208
|
+
certificateIds: [certificateId],
|
|
209
|
+
deviceIds: [appleDeviceId],
|
|
210
|
+
name: `MyApp ${Date.now()}`,
|
|
211
|
+
});
|
|
212
|
+
const profileId = (profile?.provisioningProfileId as string) ?? (profile?.profileId as string);
|
|
213
|
+
const downloaded = await downloadAppleProfile({ ...base, profileId });
|
|
214
|
+
|
|
215
|
+
const assets = await putAppleGeneratedSigningAssets({
|
|
216
|
+
bundleID: bundleId,
|
|
217
|
+
deviceUDID,
|
|
218
|
+
teamID: teamId,
|
|
219
|
+
signingMode: 'development',
|
|
220
|
+
certificateID: certificateId,
|
|
221
|
+
certificateP12Base64: certificateP12Base64!,
|
|
222
|
+
certificatePassword,
|
|
223
|
+
provisioningProfileBase64: downloaded.rawBodyBase64!,
|
|
224
|
+
profile: parseProvisioningProfileBase64(downloaded.rawBodyBase64!),
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
These are Apple's rules, not Limrun's, and each one is a common dead end:
|
|
229
|
+
|
|
230
|
+
| Rule | Consequence if ignored |
|
|
231
|
+
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
232
|
+
| A cert is useless without its private key. | You can only sign with a cert this browser created (and stored) or one imported as `.p12`. Apple never returns private keys. |
|
|
233
|
+
| Max **2** development certs per team. | Creating one per build fails with _"you already have a current Development certificate"_. Reuse, or revoke an old one to free a slot. |
|
|
234
|
+
| Profile creation needs the canonical `certificateId`. | Passing a `certRequestId` fails with _"no current certificates matching the provided certificate IDs"_. Resolve it via `listAppleCertificates`. |
|
|
235
|
+
| A revoked cert still appears in lists. | Signing with it builds fine but the device rejects install with `ApplicationVerificationFailed`. Treat revoked certs as unusable. |
|
|
236
|
+
| The team id may live in `providerId`/`publicProviderId`. | Reading only `teamId` leaves the flow stuck after sign-in. |
|
|
237
|
+
| Profiles are device-scoped and immutable. | To authorize a new device you regenerate the profile (see [Adding a device](#add-a-device-to-a-profile)). |
|
|
238
|
+
|
|
239
|
+
## Build a signed IPA
|
|
240
|
+
|
|
241
|
+
`useDeviceBuild` triggers a signed `iphoneos` build on the sandbox and streams
|
|
242
|
+
the logs. It builds for the device SDK and signs with the assets you pass.
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
import { useDeviceBuild } from '@limrun/ui/device-build/react';
|
|
246
|
+
|
|
247
|
+
const build = useDeviceBuild({ apiUrl, token, signingAssets: assets });
|
|
248
|
+
|
|
249
|
+
await build.startBuild({ signingAssets: assets });
|
|
250
|
+
// build.status: 'idle' | 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled'
|
|
251
|
+
// build.logs: BuildLogLine[] (render line.data; xcodebuild lines can be long — wrap them)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
| Field / method | Use |
|
|
255
|
+
| ------------------------------- | ------------------------------------------------------------------- |
|
|
256
|
+
| `startBuild({ signingAssets })` | Kick off the signed device build; returns the exec id. |
|
|
257
|
+
| `status` | Build lifecycle state. `succeeded` gates install. |
|
|
258
|
+
| `logs` | Streamed `command` / `stdout` / `stderr` lines. |
|
|
259
|
+
| `error` | Set (and `status` → `failed`) if the build or its log stream fails. |
|
|
260
|
+
|
|
261
|
+
## Install over WebUSB
|
|
262
|
+
|
|
263
|
+
Once the build succeeds and the device is paired, install:
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
await install.startInstallation();
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Progress streams through your `log` callback. The relay surfaces the device's
|
|
270
|
+
real reason on failure (for example
|
|
271
|
+
`Install error: ApplicationVerificationFailed — The identity used to sign the executable is no longer valid.`),
|
|
272
|
+
so render the log — it's the fastest way to diagnose a signing/profile mismatch.
|
|
273
|
+
|
|
274
|
+
## Full example
|
|
275
|
+
|
|
276
|
+
A single component covering pair → upload signing → build → install. It assumes
|
|
277
|
+
you already have `apiUrl`, `token`, and a `bundleId`.
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
import { useState } from 'react';
|
|
281
|
+
import { importSigningAssetsFromFiles, type StoredSigningAssets } from '@limrun/ui/device-build';
|
|
282
|
+
import { useDeviceBuild } from '@limrun/ui/device-build/react';
|
|
283
|
+
import { useDeviceInstallRelay } from '@limrun/ui/device-install/react';
|
|
284
|
+
|
|
285
|
+
export function InstallToIPhone({
|
|
286
|
+
apiUrl,
|
|
287
|
+
token,
|
|
288
|
+
bundleId,
|
|
289
|
+
}: {
|
|
290
|
+
apiUrl: string;
|
|
291
|
+
token?: string;
|
|
292
|
+
bundleId: string;
|
|
293
|
+
}) {
|
|
294
|
+
const [certificateFile, setCertificateFile] = useState<File>();
|
|
295
|
+
const [profileFile, setProfileFile] = useState<File>();
|
|
296
|
+
const [certificatePassword, setCertificatePassword] = useState('');
|
|
297
|
+
const [signingAssets, setSigningAssets] = useState<StoredSigningAssets>();
|
|
298
|
+
const [log, setLog] = useState<string[]>([]);
|
|
299
|
+
const append = (m: string, d?: string) => setLog((l) => [d ? `${m}: ${d}` : m, ...l]);
|
|
300
|
+
|
|
301
|
+
const install = useDeviceInstallRelay({ apiUrl, token, log: append });
|
|
302
|
+
const build = useDeviceBuild({ apiUrl, token, signingAssets });
|
|
303
|
+
|
|
304
|
+
async function prepare() {
|
|
305
|
+
if (!certificateFile || !profileFile) return;
|
|
306
|
+
setSigningAssets(
|
|
307
|
+
await importSigningAssetsFromFiles({
|
|
308
|
+
certificateFile,
|
|
309
|
+
provisioningProfileFile: profileFile,
|
|
310
|
+
certificatePassword,
|
|
311
|
+
bundleId,
|
|
312
|
+
deviceUDID: install.device?.hello.serialNumber,
|
|
313
|
+
signingMode: 'development',
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div>
|
|
320
|
+
{(install.error || build.error) && <pre>{install.error ?? build.error}</pre>}
|
|
321
|
+
|
|
322
|
+
<button disabled={!!install.busyAction} onClick={() => void install.requestUSBAccess()}>
|
|
323
|
+
Select iPhone
|
|
324
|
+
</button>
|
|
325
|
+
<button disabled={!install.canPair} onClick={() => void install.pairBrowser()}>
|
|
326
|
+
Pair
|
|
327
|
+
</button>
|
|
328
|
+
|
|
329
|
+
<input type="file" accept=".p12" onChange={(e) => setCertificateFile(e.currentTarget.files?.[0])} />
|
|
330
|
+
<input
|
|
331
|
+
type="file"
|
|
332
|
+
accept=".mobileprovision"
|
|
333
|
+
onChange={(e) => setProfileFile(e.currentTarget.files?.[0])}
|
|
334
|
+
/>
|
|
335
|
+
<input
|
|
336
|
+
type="password"
|
|
337
|
+
value={certificatePassword}
|
|
338
|
+
onChange={(e) => setCertificatePassword(e.currentTarget.value)}
|
|
339
|
+
/>
|
|
340
|
+
<button disabled={!certificateFile || !profileFile} onClick={() => void prepare()}>
|
|
341
|
+
Prepare signing assets
|
|
342
|
+
</button>
|
|
343
|
+
|
|
344
|
+
<button disabled={!signingAssets || build.status === 'running'} onClick={() => void build.startBuild()}>
|
|
345
|
+
Start signed build ({build.status})
|
|
346
|
+
</button>
|
|
347
|
+
|
|
348
|
+
<button
|
|
349
|
+
disabled={!install.canInstall || build.status !== 'succeeded'}
|
|
350
|
+
onClick={() => void install.startInstallation()}
|
|
351
|
+
>
|
|
352
|
+
Install
|
|
353
|
+
</button>
|
|
354
|
+
|
|
355
|
+
<pre>{log.join('\n')}</pre>
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
A more complete, styled reference (with the Apple Developer flow, a status
|
|
362
|
+
stepper, and error handling) lives in
|
|
363
|
+
[`src/device-install/demo`](./demo) in this package — see
|
|
364
|
+
[`demo.tsx`](./demo/demo.tsx) for the full source.
|
|
365
|
+
|
|
366
|
+
## Run the demo
|
|
367
|
+
|
|
368
|
+
The demo is a standalone Vite page you can run against any Xcode build sandbox:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
cd packages/ui
|
|
372
|
+
npm install
|
|
373
|
+
npx vite src/device-install/demo
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Open the printed `https://localhost` URL in Chrome or Edge, paste your sandbox's
|
|
377
|
+
`apiUrl` and `token`, then walk through pair → sign → build → install. The page
|
|
378
|
+
exercises the same hooks and helpers documented above, so it doubles as a
|
|
379
|
+
copy-paste reference for your own integration.
|
|
380
|
+
|
|
381
|
+
## Add a device to a profile
|
|
382
|
+
|
|
383
|
+
Provisioning profiles can't be edited in place. To authorize a new iPhone,
|
|
384
|
+
**register the UDID, then regenerate the profile** with it included:
|
|
385
|
+
|
|
386
|
+
1. `registerAppleDevice({ deviceUDID })` — add the UDID to the team.
|
|
387
|
+
2. `createAppleProfile({ ..., deviceIds: [...existing, newDeviceId] })` — mint a
|
|
388
|
+
fresh profile (unique `name`).
|
|
389
|
+
3. `downloadAppleProfile(...)` — fetch the new bytes.
|
|
390
|
+
|
|
391
|
+
This needs an Apple Developer session. A manually uploaded `.mobileprovision`
|
|
392
|
+
that's missing the device can't be amended locally — regenerate it through the
|
|
393
|
+
Apple flow.
|
|
394
|
+
|
|
395
|
+
## Storage and security
|
|
396
|
+
|
|
397
|
+
- **Pair records** and **signing assets** (`.p12`, `.mobileprovision`, and the
|
|
398
|
+
`.p12` password when given) live in the browser's IndexedDB, written only when
|
|
399
|
+
you call the storage helpers. They stay on the device unless your app moves
|
|
400
|
+
them.
|
|
401
|
+
- The **Apple ID password never reaches Limrun** — only SRP proof material does.
|
|
402
|
+
- The sandbox `token` is instance-scoped; the Apple session is held in the
|
|
403
|
+
browser. Treat any `.p12` and password your app handles as secrets, and serve
|
|
404
|
+
the UI over HTTPS in production.
|
|
405
|
+
|
|
406
|
+
## Troubleshooting
|
|
407
|
+
|
|
408
|
+
| Symptom | Cause and fix |
|
|
409
|
+
| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
410
|
+
| `WebUSB is not available` | Use Chrome/Edge on HTTPS or `localhost`. Safari/Firefox are unsupported. |
|
|
411
|
+
| `Unable to claim interface` / "operation in progress" | Unplug/replug the iPhone, unlock it, close other WebUSB tabs, retry. Make sure you're on a current `@limrun/ui`. |
|
|
412
|
+
| First transfer fails with _"endpoint is not part of a claimed and selected alternate interface"_ | Update `@limrun/ui` — recent versions explicitly select the usbmux alternate (incl. alt 0). |
|
|
413
|
+
| Pairing seems to hang | The user must unlock the iPhone and tap **Trust**; the first attempt can fail if the prompt isn't accepted in time. Retry. |
|
|
414
|
+
| `no synced folder found; call /sync first` | Sync your project into the sandbox first (`lim xcode build .` / `lim xcode sync .`). |
|
|
415
|
+
| Install fails, log shows `ApplicationVerificationFailed` | Signed with a **revoked/invalid cert**, or the **device isn't in the profile**, or the **bundle ID isn't covered**. `zsign` doesn't rewrite the bundle ID — it must match (or be wildcarded by) the profile. |
|
|
416
|
+
| `no current certificates matching the provided certificate IDs` | You passed a `certRequestId`, or the cert was revoked. Resolve the canonical `certificateId` via `listAppleCertificates`. |
|
|
417
|
+
| `You already have a current Development certificate…` | Apple's cert cap. Reuse a stored cert, upload a `.p12`, or revoke one at developer.apple.com. |
|
|
418
|
+
| `Multiple profiles found with the name '…'` | Give created profiles a unique name. |
|
|
419
|
+
| Apple flow does nothing after sign-in | Resolve the team id across `teamId`/`providerId`/`publicProviderId`. |
|
|
420
|
+
|
|
421
|
+
## Next steps
|
|
422
|
+
|
|
423
|
+
Provision an Xcode sandbox, sync source, and produce signed IPAs. Drive a simulator for the same app: taps, screenshots, logs, recordings. Build every PR and post a live preview link from a GitHub workflow.
|