@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,23 +1,41 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
closeDeviceRelayTarget,
|
|
4
|
+
createBundleIDRequest,
|
|
5
|
+
createDevelopmentProfileRequest,
|
|
6
|
+
downloadCertificateRequest,
|
|
7
|
+
downloadProfileRequest,
|
|
8
|
+
exportAppleCertificateP12,
|
|
4
9
|
fetchLimbuildInfo,
|
|
10
|
+
findDevelopmentCertificatesRequest,
|
|
11
|
+
findBundleIDRequest,
|
|
12
|
+
findDeviceRequest,
|
|
13
|
+
findDevelopmentProfilesRequest,
|
|
14
|
+
generateAppleSigningKeyAndCSR,
|
|
5
15
|
getPairRecord,
|
|
6
16
|
getLatestSigningAssets,
|
|
17
|
+
getLatestSigningAssetsWithCertificate,
|
|
7
18
|
getReusableAppleSigningAssets,
|
|
8
19
|
listTeamsRequest,
|
|
9
20
|
parseProvisioningProfile,
|
|
21
|
+
parseProvisioningProfileBase64,
|
|
10
22
|
profileContainsDevice,
|
|
11
23
|
proxyProvisioningRequest,
|
|
24
|
+
putAppleGeneratedSigningAssets,
|
|
12
25
|
putPairRecord,
|
|
13
26
|
putSigningAssets,
|
|
27
|
+
registerDeviceRequest,
|
|
14
28
|
requestUSBAccess as requestDeviceUSBAccess,
|
|
15
29
|
startBrowserOwnedAppleIDLogin,
|
|
16
30
|
startSignedDeviceBuild,
|
|
17
31
|
startInstallRelay,
|
|
18
32
|
startPairingRelay,
|
|
33
|
+
submitDevelopmentCSRRequest,
|
|
19
34
|
watchBuildLogEvents,
|
|
20
35
|
type AppleIDLoginResult,
|
|
36
|
+
type AppleDeveloperPortalDevice,
|
|
37
|
+
type AppleDeveloperPortalAppID,
|
|
38
|
+
type AppleDeveloperPortalResponse,
|
|
21
39
|
type AppleDeveloperPortalTeam,
|
|
22
40
|
type BuildLogLine,
|
|
23
41
|
type DeviceInstallBuildStatus,
|
|
@@ -32,6 +50,13 @@ import type { RelayClient } from '../core/device-install/operations';
|
|
|
32
50
|
|
|
33
51
|
type DeviceInstallStepStatuses = Record<DeviceInstallStep, DeviceInstallStepStatus>;
|
|
34
52
|
|
|
53
|
+
type ReusableAppleCertificate = Pick<
|
|
54
|
+
StoredSigningAssets,
|
|
55
|
+
'certificateID' | 'certificateP12Base64' | 'certificatePassword' | 'teamID'
|
|
56
|
+
> & {
|
|
57
|
+
certificateID: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
35
60
|
export type UseDeviceInstallOptions = {
|
|
36
61
|
apiUrl?: string;
|
|
37
62
|
token?: string;
|
|
@@ -43,26 +68,40 @@ export type UseDeviceInstallResult = {
|
|
|
43
68
|
device?: DeviceInstallDevice;
|
|
44
69
|
hasPairRecord: boolean;
|
|
45
70
|
hasSigningAssets: boolean;
|
|
71
|
+
hasSigningInputs: boolean;
|
|
46
72
|
pairConfirmationRequired: boolean;
|
|
47
73
|
logs: string[];
|
|
48
74
|
buildLogs: BuildLogLine[];
|
|
49
75
|
buildStatus: DeviceInstallBuildStatus;
|
|
50
76
|
appleSigningStatus: DeviceInstallAppleSigningStatus;
|
|
51
77
|
appleTeams: AppleDeveloperPortalTeam[];
|
|
78
|
+
appleDevices: AppleDeveloperPortalDevice[];
|
|
79
|
+
appleAppIDs: AppleDeveloperPortalAppID[];
|
|
80
|
+
applePortalSummary?: ApplePortalSummary;
|
|
52
81
|
selectedAppleTeamID?: string;
|
|
82
|
+
selectedAppleDeviceIDs: string[];
|
|
83
|
+
connectedAppleDeviceRegistered: boolean;
|
|
84
|
+
connectedDeviceInProfile?: boolean;
|
|
85
|
+
hasReusableAppleCertificate: boolean;
|
|
86
|
+
appleBundleID: string;
|
|
53
87
|
buildLogPanelOpen: boolean;
|
|
54
88
|
busyAction?: DeviceInstallBusyAction;
|
|
55
89
|
error?: string;
|
|
56
90
|
canBuild: boolean;
|
|
91
|
+
canPrepareAppleSigningAssets: boolean;
|
|
57
92
|
canRequestUSBAccess: boolean;
|
|
58
93
|
canPairBrowser: boolean;
|
|
59
94
|
canInstall: boolean;
|
|
60
95
|
setSigningFiles: (files: DeviceInstallSigningFiles) => void;
|
|
96
|
+
setAppleBundleID: (bundleID: string) => void;
|
|
97
|
+
setSelectedAppleDeviceIDs: (deviceIDs: string[]) => void;
|
|
61
98
|
setBuildLogPanelOpen: (open: boolean) => void;
|
|
62
99
|
startAppleIDLogin: (input: DeviceInstallAppleIDLoginInput) => Promise<void>;
|
|
63
100
|
submitAppleTwoFactorCode: (code: string) => Promise<void>;
|
|
64
101
|
setSelectedAppleTeamID: (teamID: string | undefined) => void;
|
|
65
|
-
|
|
102
|
+
clearAppleIDLogin: () => void;
|
|
103
|
+
registerConnectedAppleDevice: () => Promise<void>;
|
|
104
|
+
prepareAppleSigningAssets: () => Promise<void>;
|
|
66
105
|
startDeviceBuild: () => Promise<void>;
|
|
67
106
|
requestUSBAccess: () => Promise<void>;
|
|
68
107
|
pairBrowser: () => Promise<void>;
|
|
@@ -76,6 +115,7 @@ export type DeviceInstallAppleSigningStatus =
|
|
|
76
115
|
| 'two-factor-required'
|
|
77
116
|
| 'authenticated'
|
|
78
117
|
| 'preparing-assets'
|
|
118
|
+
| 'assets-ready'
|
|
79
119
|
| 'using-cached-profile'
|
|
80
120
|
| 'error';
|
|
81
121
|
|
|
@@ -96,10 +136,15 @@ export type DeviceInstallAppleIDLoginInput = {
|
|
|
96
136
|
password: string;
|
|
97
137
|
};
|
|
98
138
|
|
|
139
|
+
export type ApplePortalSummary = {
|
|
140
|
+
certificateCount: number;
|
|
141
|
+
profileCount: number;
|
|
142
|
+
};
|
|
143
|
+
|
|
99
144
|
const initialStepStatuses: DeviceInstallStepStatuses = {
|
|
145
|
+
signing: 'idle',
|
|
146
|
+
connect: 'idle',
|
|
100
147
|
build: 'idle',
|
|
101
|
-
usb: 'idle',
|
|
102
|
-
pair: 'idle',
|
|
103
148
|
install: 'idle',
|
|
104
149
|
};
|
|
105
150
|
|
|
@@ -107,19 +152,25 @@ export function useDeviceInstall({
|
|
|
107
152
|
apiUrl,
|
|
108
153
|
token,
|
|
109
154
|
}: UseDeviceInstallOptions): UseDeviceInstallResult {
|
|
110
|
-
const [currentStep, setCurrentStep] = useState<DeviceInstallStep>('
|
|
155
|
+
const [currentStep, setCurrentStep] = useState<DeviceInstallStep>('signing');
|
|
111
156
|
const [stepStatuses, setStepStatuses] = useState<DeviceInstallStepStatuses>(initialStepStatuses);
|
|
112
157
|
const [selectedDevice, setSelectedDevice] = useState<DeviceRelayTarget | undefined>();
|
|
113
158
|
const [pairRecord, setPairRecord] = useState<StoredPairRecord | undefined>();
|
|
114
159
|
const [signingAssets, setSigningAssets] = useState<StoredSigningAssets | undefined>();
|
|
115
160
|
const [logs, setLogs] = useState<string[]>([
|
|
116
|
-
'Ready.
|
|
161
|
+
'Ready. Prepare signing assets, connect and pair the iPhone, build, then install.',
|
|
117
162
|
]);
|
|
118
163
|
const [buildLogs, setBuildLogs] = useState<BuildLogLine[]>([]);
|
|
119
164
|
const [buildStatus, setBuildStatus] = useState<DeviceInstallBuildStatus>('idle');
|
|
120
165
|
const [appleSigningStatus, setAppleSigningStatus] = useState<DeviceInstallAppleSigningStatus>('idle');
|
|
121
166
|
const [appleTeams, setAppleTeams] = useState<AppleDeveloperPortalTeam[]>([]);
|
|
167
|
+
const [appleDevices, setAppleDevices] = useState<AppleDeveloperPortalDevice[]>([]);
|
|
168
|
+
const [appleAppIDs, setAppleAppIDs] = useState<AppleDeveloperPortalAppID[]>([]);
|
|
169
|
+
const [selectedAppleDeviceIDs, setSelectedAppleDeviceIDs] = useState<string[]>([]);
|
|
170
|
+
const [applePortalSummary, setApplePortalSummary] = useState<ApplePortalSummary | undefined>();
|
|
122
171
|
const [selectedAppleTeamID, setSelectedAppleTeamID] = useState<string | undefined>();
|
|
172
|
+
const [appleBundleID, setAppleBundleID] = useState('');
|
|
173
|
+
const [reusableAppleCertificate, setReusableAppleCertificate] = useState<ReusableAppleCertificate | undefined>();
|
|
123
174
|
const [buildLogPanelOpen, setBuildLogPanelOpen] = useState(false);
|
|
124
175
|
const [busyAction, setBusyAction] = useState<DeviceInstallBusyAction | undefined>();
|
|
125
176
|
const [error, setError] = useState<string | undefined>();
|
|
@@ -128,7 +179,7 @@ export function useDeviceInstall({
|
|
|
128
179
|
const relayRef = useRef<RelayClient | undefined>(undefined);
|
|
129
180
|
const selectedDeviceRef = useRef<DeviceRelayTarget | undefined>(undefined);
|
|
130
181
|
const stopBuildWatcherRef = useRef<(() => void) | undefined>(undefined);
|
|
131
|
-
const
|
|
182
|
+
const appleIDLoginRef = useRef<AppleIDLoginResult | undefined>(undefined);
|
|
132
183
|
|
|
133
184
|
const log = useCallback((message: string, detail?: string) => {
|
|
134
185
|
const line = detail ? `${message}\n${detail}` : message;
|
|
@@ -140,9 +191,67 @@ export function useDeviceInstall({
|
|
|
140
191
|
}, []);
|
|
141
192
|
|
|
142
193
|
const setSigningFiles = useCallback((files: DeviceInstallSigningFiles) => {
|
|
143
|
-
setSigningFilesState((current) =>
|
|
194
|
+
setSigningFilesState((current) => {
|
|
195
|
+
const next = { ...current, ...files };
|
|
196
|
+
const ready = !!next.certificateFile && !!next.provisioningProfileFile && !!next.certificatePassword;
|
|
197
|
+
setStepStatus('signing', ready ? 'complete' : 'active');
|
|
198
|
+
if (ready) {
|
|
199
|
+
setAppleSigningStatus('assets-ready');
|
|
200
|
+
setCurrentStep('connect');
|
|
201
|
+
}
|
|
202
|
+
return next;
|
|
203
|
+
});
|
|
144
204
|
setSigningAssets(undefined);
|
|
145
|
-
}, []);
|
|
205
|
+
}, [setStepStatus]);
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
let cancelled = false;
|
|
209
|
+
void getLatestSigningAssets().then((stored) => {
|
|
210
|
+
if (cancelled || !stored) return;
|
|
211
|
+
setSigningAssets(stored);
|
|
212
|
+
setAppleBundleID(stored.bundleID);
|
|
213
|
+
setAppleSigningStatus('using-cached-profile');
|
|
214
|
+
setStepStatus('signing', 'complete');
|
|
215
|
+
setCurrentStep('connect');
|
|
216
|
+
log('Using stored signing assets', stored.bundleID);
|
|
217
|
+
});
|
|
218
|
+
return () => {
|
|
219
|
+
cancelled = true;
|
|
220
|
+
};
|
|
221
|
+
}, [log, setStepStatus]);
|
|
222
|
+
|
|
223
|
+
const selectedDeveloperTeamID = useCallback(() => {
|
|
224
|
+
return developerPortalTeamID(appleTeams.find((team) => appleTeamSelectionID(team) === selectedAppleTeamID));
|
|
225
|
+
}, [appleTeams, selectedAppleTeamID]);
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
const teamID = selectedDeveloperTeamID();
|
|
229
|
+
if (!teamID) {
|
|
230
|
+
setReusableAppleCertificate(undefined);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
let cancelled = false;
|
|
234
|
+
void findReusableAppleCertificate(teamID).then((certificate) => {
|
|
235
|
+
if (!cancelled) {
|
|
236
|
+
setReusableAppleCertificate(certificate);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
return () => {
|
|
240
|
+
cancelled = true;
|
|
241
|
+
};
|
|
242
|
+
}, [selectedDeveloperTeamID]);
|
|
243
|
+
|
|
244
|
+
const connectedAppleDevice = selectedDevice?.hello.serialNumber
|
|
245
|
+
? appleDevices.find((device) => normalizeAppleUDID(device.deviceNumber) === normalizeAppleUDID(selectedDevice.hello.serialNumber))
|
|
246
|
+
: undefined;
|
|
247
|
+
const connectedAppleDeviceRegistered = !!connectedAppleDevice?.deviceId;
|
|
248
|
+
const manualSigningFilesReady =
|
|
249
|
+
!!signingFiles.certificateFile && !!signingFiles.provisioningProfileFile && !!signingFiles.certificatePassword;
|
|
250
|
+
const signingInputsReady = !!signingAssets || manualSigningFilesReady;
|
|
251
|
+
const connectedDeviceInProfile =
|
|
252
|
+
selectedDevice?.hello.serialNumber && signingAssets
|
|
253
|
+
? profileContainsDevice(signingAssets.profile, selectedDevice.hello.serialNumber)
|
|
254
|
+
: undefined;
|
|
146
255
|
|
|
147
256
|
useEffect(() => {
|
|
148
257
|
selectedDeviceRef.current = selectedDevice;
|
|
@@ -157,16 +266,19 @@ export function useDeviceInstall({
|
|
|
157
266
|
useEffect(() => {
|
|
158
267
|
return () => {
|
|
159
268
|
stopBuildWatcherRef.current?.();
|
|
160
|
-
void
|
|
269
|
+
void appleIDLoginRef.current?.close();
|
|
270
|
+
appleIDLoginRef.current = undefined;
|
|
161
271
|
void cleanupDeviceAccess();
|
|
162
272
|
};
|
|
163
273
|
}, [cleanupDeviceAccess]);
|
|
164
274
|
|
|
165
275
|
const resolveSigningAssetsForBuild = useCallback(async () => {
|
|
166
|
-
const
|
|
167
|
-
|
|
276
|
+
const requestedBundleID = appleBundleID.trim();
|
|
277
|
+
const info = requestedBundleID ? undefined : apiUrl ? await fetchStoredBuildInfo(apiUrl, token).catch(() => undefined) : undefined;
|
|
278
|
+
const bundleID = requestedBundleID || info?.lastBuildConfig?.bundleId;
|
|
279
|
+
if (bundleID) {
|
|
168
280
|
const cached = await getReusableAppleSigningAssets({
|
|
169
|
-
bundleID
|
|
281
|
+
bundleID,
|
|
170
282
|
deviceUDID: selectedDevice?.hello.serialNumber,
|
|
171
283
|
teamID: selectedAppleTeamID,
|
|
172
284
|
});
|
|
@@ -213,21 +325,43 @@ export function useDeviceInstall({
|
|
|
213
325
|
setSigningAssets(storedAssets);
|
|
214
326
|
log('Signing assets stored locally', storageBundleId);
|
|
215
327
|
return storedAssets;
|
|
216
|
-
}, [apiUrl, log, selectedAppleTeamID, selectedDevice?.hello.serialNumber, signingFiles, token]);
|
|
328
|
+
}, [apiUrl, appleBundleID, log, selectedAppleTeamID, selectedDevice?.hello.serialNumber, signingFiles, token]);
|
|
217
329
|
|
|
218
330
|
const startAppleIDLogin = useCallback(
|
|
219
331
|
async ({ accountName, password }: DeviceInstallAppleIDLoginInput) => {
|
|
220
332
|
if (!apiUrl) return;
|
|
221
|
-
setBusyAction('
|
|
333
|
+
setBusyAction('signing');
|
|
222
334
|
setError(undefined);
|
|
335
|
+
setCurrentStep('signing');
|
|
336
|
+
setStepStatus('signing', 'active');
|
|
223
337
|
setAppleSigningStatus('authenticating');
|
|
224
338
|
try {
|
|
225
|
-
await
|
|
339
|
+
await appleIDLoginRef.current?.close().catch(() => undefined);
|
|
226
340
|
const session = await startBrowserOwnedAppleIDLogin({ limbuildApiUrl: apiUrl, token, accountName, password });
|
|
227
|
-
|
|
341
|
+
appleIDLoginRef.current = session;
|
|
228
342
|
if (!session.requiresTwoFactor) {
|
|
229
|
-
await session.finalize();
|
|
230
|
-
await refreshAppleTeams(
|
|
343
|
+
const accountSession = await session.finalize().catch(() => undefined);
|
|
344
|
+
const teamID = await refreshAppleTeams(
|
|
345
|
+
apiUrl,
|
|
346
|
+
session.appleSessionId,
|
|
347
|
+
token,
|
|
348
|
+
setAppleTeams,
|
|
349
|
+
setSelectedAppleTeamID,
|
|
350
|
+
accountSession?.body as AppleDeveloperPortalResponse | undefined,
|
|
351
|
+
);
|
|
352
|
+
await refreshAppleAppIDs(apiUrl, session.appleSessionId, token, teamID, setAppleAppIDs, setAppleBundleID);
|
|
353
|
+
if (teamID) {
|
|
354
|
+
await refreshAppleDevices({
|
|
355
|
+
apiUrl,
|
|
356
|
+
token,
|
|
357
|
+
appleSessionId: session.appleSessionId,
|
|
358
|
+
teamID,
|
|
359
|
+
setAppleDevices,
|
|
360
|
+
setSelectedAppleDeviceIDs,
|
|
361
|
+
log,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
await refreshApplePortalSummary(apiUrl, session.appleSessionId, token, teamID, setApplePortalSummary, log);
|
|
231
365
|
}
|
|
232
366
|
setAppleSigningStatus(session.requiresTwoFactor ? 'two-factor-required' : 'authenticated');
|
|
233
367
|
log(
|
|
@@ -243,22 +377,44 @@ export function useDeviceInstall({
|
|
|
243
377
|
setBusyAction(undefined);
|
|
244
378
|
}
|
|
245
379
|
},
|
|
246
|
-
[apiUrl, log, token],
|
|
380
|
+
[apiUrl, log, setStepStatus, token],
|
|
247
381
|
);
|
|
248
382
|
|
|
249
383
|
const submitAppleTwoFactorCode = useCallback(
|
|
250
384
|
async (code: string) => {
|
|
251
|
-
const session =
|
|
385
|
+
const session = appleIDLoginRef.current;
|
|
252
386
|
if (!session) {
|
|
253
387
|
throw new Error('Start Apple ID login before submitting a two-factor code.');
|
|
254
388
|
}
|
|
255
|
-
setBusyAction('
|
|
389
|
+
setBusyAction('signing');
|
|
256
390
|
setError(undefined);
|
|
391
|
+
setCurrentStep('signing');
|
|
392
|
+
setStepStatus('signing', 'active');
|
|
257
393
|
try {
|
|
258
394
|
await session.finishTwoFactor(code);
|
|
259
|
-
await session.finalize();
|
|
260
395
|
if (apiUrl) {
|
|
261
|
-
await
|
|
396
|
+
const accountSession = await session.finalize().catch(() => undefined);
|
|
397
|
+
const teamID = await refreshAppleTeams(
|
|
398
|
+
apiUrl,
|
|
399
|
+
session.appleSessionId,
|
|
400
|
+
token,
|
|
401
|
+
setAppleTeams,
|
|
402
|
+
setSelectedAppleTeamID,
|
|
403
|
+
accountSession?.body as AppleDeveloperPortalResponse | undefined,
|
|
404
|
+
);
|
|
405
|
+
await refreshAppleAppIDs(apiUrl, session.appleSessionId, token, teamID, setAppleAppIDs, setAppleBundleID);
|
|
406
|
+
if (teamID) {
|
|
407
|
+
await refreshAppleDevices({
|
|
408
|
+
apiUrl,
|
|
409
|
+
token,
|
|
410
|
+
appleSessionId: session.appleSessionId,
|
|
411
|
+
teamID,
|
|
412
|
+
setAppleDevices,
|
|
413
|
+
setSelectedAppleDeviceIDs,
|
|
414
|
+
log,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
await refreshApplePortalSummary(apiUrl, session.appleSessionId, token, teamID, setApplePortalSummary, log);
|
|
262
418
|
}
|
|
263
419
|
setAppleSigningStatus('authenticated');
|
|
264
420
|
log('Apple ID two-factor authentication accepted');
|
|
@@ -271,17 +427,181 @@ export function useDeviceInstall({
|
|
|
271
427
|
setBusyAction(undefined);
|
|
272
428
|
}
|
|
273
429
|
},
|
|
274
|
-
[apiUrl, log, token],
|
|
430
|
+
[apiUrl, log, setStepStatus, token],
|
|
275
431
|
);
|
|
276
432
|
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
433
|
+
const clearAppleIDLogin = useCallback(() => {
|
|
434
|
+
void appleIDLoginRef.current?.close();
|
|
435
|
+
appleIDLoginRef.current = undefined;
|
|
280
436
|
setAppleTeams([]);
|
|
437
|
+
setAppleDevices([]);
|
|
438
|
+
setAppleAppIDs([]);
|
|
439
|
+
setSelectedAppleDeviceIDs([]);
|
|
440
|
+
setApplePortalSummary(undefined);
|
|
281
441
|
setSelectedAppleTeamID(undefined);
|
|
442
|
+
setReusableAppleCertificate(undefined);
|
|
282
443
|
setAppleSigningStatus('idle');
|
|
283
|
-
|
|
284
|
-
|
|
444
|
+
setStepStatus('signing', 'idle');
|
|
445
|
+
log('Apple ID login state cleared');
|
|
446
|
+
}, [log, setStepStatus]);
|
|
447
|
+
|
|
448
|
+
const selectAppleTeam = useCallback(
|
|
449
|
+
(teamID: string | undefined) => {
|
|
450
|
+
setSelectedAppleTeamID(teamID);
|
|
451
|
+
setAppleAppIDs([]);
|
|
452
|
+
setAppleDevices([]);
|
|
453
|
+
setSelectedAppleDeviceIDs([]);
|
|
454
|
+
setApplePortalSummary(undefined);
|
|
455
|
+
setAppleBundleID('');
|
|
456
|
+
setSigningAssets(undefined);
|
|
457
|
+
const session = appleIDLoginRef.current;
|
|
458
|
+
if (!apiUrl || !session || !teamID) return;
|
|
459
|
+
const developerTeamID = developerPortalTeamID(appleTeams.find((team) => appleTeamSelectionID(team) === teamID));
|
|
460
|
+
if (!developerTeamID) return;
|
|
461
|
+
void (async () => {
|
|
462
|
+
try {
|
|
463
|
+
await refreshAppleAppIDs(apiUrl, session.appleSessionId, token, developerTeamID, setAppleAppIDs, setAppleBundleID);
|
|
464
|
+
await refreshApplePortalSummary(apiUrl, session.appleSessionId, token, developerTeamID, setApplePortalSummary, log);
|
|
465
|
+
await refreshAppleDevices({
|
|
466
|
+
apiUrl,
|
|
467
|
+
token,
|
|
468
|
+
appleSessionId: session.appleSessionId,
|
|
469
|
+
teamID: developerTeamID,
|
|
470
|
+
connectedUDID: selectedDeviceRef.current?.hello.serialNumber,
|
|
471
|
+
setAppleDevices,
|
|
472
|
+
setSelectedAppleDeviceIDs,
|
|
473
|
+
log,
|
|
474
|
+
});
|
|
475
|
+
} catch (caught) {
|
|
476
|
+
const message = errorMessage(caught);
|
|
477
|
+
setError(message);
|
|
478
|
+
log('Apple team refresh failed', message);
|
|
479
|
+
}
|
|
480
|
+
})();
|
|
481
|
+
},
|
|
482
|
+
[apiUrl, appleTeams, log, token],
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const registerConnectedAppleDevice = useCallback(async () => {
|
|
486
|
+
const teamID = selectedDeveloperTeamID();
|
|
487
|
+
if (!apiUrl || !appleIDLoginRef.current || !selectedDevice?.hello.serialNumber || !teamID) return;
|
|
488
|
+
setBusyAction('signing');
|
|
489
|
+
setError(undefined);
|
|
490
|
+
try {
|
|
491
|
+
const normalizedUDID = normalizeAppleUDID(selectedDevice.hello.serialNumber);
|
|
492
|
+
const created = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
493
|
+
apiUrl,
|
|
494
|
+
appleIDLoginRef.current.appleSessionId,
|
|
495
|
+
registerDeviceRequest({
|
|
496
|
+
deviceUDID: normalizedUDID,
|
|
497
|
+
teamID,
|
|
498
|
+
name: selectedDevice.hello.productName ?? 'Limrun iPhone',
|
|
499
|
+
}),
|
|
500
|
+
token,
|
|
501
|
+
);
|
|
502
|
+
assertApplePortalResponseOK(created.body, 'Apple device registration');
|
|
503
|
+
await refreshAppleDevices({
|
|
504
|
+
apiUrl,
|
|
505
|
+
token,
|
|
506
|
+
appleSessionId: appleIDLoginRef.current.appleSessionId,
|
|
507
|
+
teamID,
|
|
508
|
+
connectedUDID: selectedDevice.hello.serialNumber,
|
|
509
|
+
setAppleDevices,
|
|
510
|
+
setSelectedAppleDeviceIDs,
|
|
511
|
+
log,
|
|
512
|
+
});
|
|
513
|
+
log('Connected iPhone registered with Apple Developer', normalizedUDID);
|
|
514
|
+
} catch (caught) {
|
|
515
|
+
const message = errorMessage(caught);
|
|
516
|
+
setError(message);
|
|
517
|
+
log('Apple device registration failed', message);
|
|
518
|
+
} finally {
|
|
519
|
+
setBusyAction(undefined);
|
|
520
|
+
}
|
|
521
|
+
}, [apiUrl, log, selectedDeveloperTeamID, selectedDevice?.hello.productName, selectedDevice?.hello.serialNumber, token]);
|
|
522
|
+
|
|
523
|
+
const prepareAppleSigningAssets = useCallback(async () => {
|
|
524
|
+
if (!apiUrl || !appleIDLoginRef.current) return;
|
|
525
|
+
const bundleID = appleBundleID.trim();
|
|
526
|
+
if (!bundleID) {
|
|
527
|
+
throw new Error('Enter a bundle ID before preparing signing assets.');
|
|
528
|
+
}
|
|
529
|
+
if (!selectedAppleTeamID) {
|
|
530
|
+
throw new Error('Select an Apple Developer team before preparing signing assets.');
|
|
531
|
+
}
|
|
532
|
+
const teamID = selectedDeveloperTeamID();
|
|
533
|
+
if (!teamID) {
|
|
534
|
+
throw new Error('Selected Apple team does not include a Developer Portal team ID.');
|
|
535
|
+
}
|
|
536
|
+
if (selectedAppleDeviceIDs.length === 0) {
|
|
537
|
+
throw new Error('Select at least one Apple Developer device before preparing signing assets.');
|
|
538
|
+
}
|
|
539
|
+
if (!reusableAppleCertificate && !signingFiles.certificatePassword) {
|
|
540
|
+
throw new Error('Enter a .p12 password before preparing signing assets.');
|
|
541
|
+
}
|
|
542
|
+
const selectedPortalDevice = appleDevices.find((device) => selectedAppleDeviceIDs.includes(device.deviceId ?? ''));
|
|
543
|
+
const signingDeviceUDID = selectedDevice?.hello.serialNumber ?? selectedPortalDevice?.deviceNumber;
|
|
544
|
+
if (!signingDeviceUDID) {
|
|
545
|
+
throw new Error('Select an Apple Developer device before preparing signing assets.');
|
|
546
|
+
}
|
|
547
|
+
setBusyAction('signing');
|
|
548
|
+
setError(undefined);
|
|
549
|
+
setCurrentStep('signing');
|
|
550
|
+
setStepStatus('signing', 'active');
|
|
551
|
+
setAppleSigningStatus('preparing-assets');
|
|
552
|
+
try {
|
|
553
|
+
const cached = await getReusableAppleSigningAssets({
|
|
554
|
+
bundleID,
|
|
555
|
+
deviceUDID: signingDeviceUDID,
|
|
556
|
+
teamID,
|
|
557
|
+
});
|
|
558
|
+
if (cached) {
|
|
559
|
+
setSigningAssets(cached);
|
|
560
|
+
setAppleSigningStatus('assets-ready');
|
|
561
|
+
setStepStatus('signing', 'complete');
|
|
562
|
+
setCurrentStep('connect');
|
|
563
|
+
log('Using cached Apple signing assets', bundleID);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const assets = await prepareAppleSigningAssetsForDevice({
|
|
567
|
+
apiUrl,
|
|
568
|
+
token,
|
|
569
|
+
appleSessionId: appleIDLoginRef.current.appleSessionId,
|
|
570
|
+
teamID,
|
|
571
|
+
bundleID,
|
|
572
|
+
deviceUDID: signingDeviceUDID,
|
|
573
|
+
deviceIDs: selectedAppleDeviceIDs,
|
|
574
|
+
certificatePassword: signingFiles.certificatePassword,
|
|
575
|
+
reusableCertificate: reusableAppleCertificate,
|
|
576
|
+
});
|
|
577
|
+
setSigningAssets(assets);
|
|
578
|
+
setAppleSigningStatus('assets-ready');
|
|
579
|
+
setStepStatus('signing', 'complete');
|
|
580
|
+
setCurrentStep('connect');
|
|
581
|
+
log('Apple signing assets stored locally', `${bundleID} for ${signingDeviceUDID}`);
|
|
582
|
+
} catch (caught) {
|
|
583
|
+
const message = errorMessage(caught);
|
|
584
|
+
setError(message);
|
|
585
|
+
setAppleSigningStatus('error');
|
|
586
|
+
log('Apple signing asset preparation failed', message);
|
|
587
|
+
} finally {
|
|
588
|
+
setBusyAction(undefined);
|
|
589
|
+
}
|
|
590
|
+
}, [
|
|
591
|
+
apiUrl,
|
|
592
|
+
appleBundleID,
|
|
593
|
+
appleDevices,
|
|
594
|
+
appleTeams,
|
|
595
|
+
log,
|
|
596
|
+
selectedAppleTeamID,
|
|
597
|
+
selectedAppleDeviceIDs,
|
|
598
|
+
selectedDeveloperTeamID,
|
|
599
|
+
selectedDevice?.hello.serialNumber,
|
|
600
|
+
setStepStatus,
|
|
601
|
+
reusableAppleCertificate,
|
|
602
|
+
signingFiles.certificatePassword,
|
|
603
|
+
token,
|
|
604
|
+
]);
|
|
285
605
|
|
|
286
606
|
const startDeviceBuild = useCallback(async () => {
|
|
287
607
|
if (!apiUrl) return;
|
|
@@ -316,7 +636,7 @@ export function useDeviceInstall({
|
|
|
316
636
|
setBuildStatus(status);
|
|
317
637
|
if (status === 'succeeded') {
|
|
318
638
|
setStepStatus('build', 'complete');
|
|
319
|
-
setCurrentStep('
|
|
639
|
+
setCurrentStep('install');
|
|
320
640
|
} else if (status === 'failed' || status === 'cancelled') {
|
|
321
641
|
setStepStatus('build', 'error');
|
|
322
642
|
}
|
|
@@ -336,13 +656,13 @@ export function useDeviceInstall({
|
|
|
336
656
|
} finally {
|
|
337
657
|
setBusyAction(undefined);
|
|
338
658
|
}
|
|
339
|
-
}, [apiUrl, log, resolveSigningAssetsForBuild, setStepStatus, token]);
|
|
659
|
+
}, [apiUrl, log, pairRecord, resolveSigningAssetsForBuild, setStepStatus, token]);
|
|
340
660
|
|
|
341
661
|
const requestUSBAccess = useCallback(async () => {
|
|
342
662
|
setBusyAction('usb');
|
|
343
663
|
setError(undefined);
|
|
344
|
-
setCurrentStep('
|
|
345
|
-
setStepStatus('
|
|
664
|
+
setCurrentStep('connect');
|
|
665
|
+
setStepStatus('connect', 'active');
|
|
346
666
|
try {
|
|
347
667
|
await cleanupDeviceAccess();
|
|
348
668
|
const target = await requestDeviceUSBAccess({ log });
|
|
@@ -350,33 +670,48 @@ export function useDeviceInstall({
|
|
|
350
670
|
setPairConfirmationRequired(false);
|
|
351
671
|
const storedPairRecord = await getPairRecord(target.hello.serialNumber);
|
|
352
672
|
setPairRecord(storedPairRecord);
|
|
353
|
-
const storedSigningAssets = await getLatestSigningAssets();
|
|
673
|
+
const storedSigningAssets = manualSigningFilesReady ? undefined : await getLatestSigningAssets();
|
|
354
674
|
if (storedSigningAssets) {
|
|
355
675
|
if (!profileContainsDevice(storedSigningAssets.profile, target.hello.serialNumber)) {
|
|
356
676
|
throw new Error('Stored provisioning profile does not include the selected iPhone.');
|
|
357
677
|
}
|
|
358
678
|
setSigningAssets(storedSigningAssets);
|
|
359
679
|
}
|
|
360
|
-
|
|
361
|
-
|
|
680
|
+
if (apiUrl && appleIDLoginRef.current) {
|
|
681
|
+
const teamID = selectedDeveloperTeamID();
|
|
682
|
+
if (teamID) {
|
|
683
|
+
await refreshAppleDevices({
|
|
684
|
+
apiUrl,
|
|
685
|
+
token,
|
|
686
|
+
appleSessionId: appleIDLoginRef.current.appleSessionId,
|
|
687
|
+
teamID,
|
|
688
|
+
connectedUDID: target.hello.serialNumber,
|
|
689
|
+
setAppleDevices,
|
|
690
|
+
setSelectedAppleDeviceIDs,
|
|
691
|
+
log,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
setStepStatus('connect', storedPairRecord ? 'complete' : 'active');
|
|
696
|
+
setCurrentStep(storedPairRecord ? 'build' : 'connect');
|
|
362
697
|
log(storedPairRecord ? 'Pair record found' : 'No pair record found', target.hello.serialNumber);
|
|
363
698
|
} catch (caught) {
|
|
364
699
|
const message = errorMessage(caught);
|
|
365
700
|
setError(message);
|
|
366
|
-
setStepStatus('
|
|
701
|
+
setStepStatus('connect', 'error');
|
|
367
702
|
log('USB access failed', message);
|
|
368
703
|
} finally {
|
|
369
704
|
setBusyAction(undefined);
|
|
370
705
|
}
|
|
371
|
-
}, [cleanupDeviceAccess, log, setStepStatus]);
|
|
706
|
+
}, [apiUrl, cleanupDeviceAccess, log, manualSigningFilesReady, selectedDeveloperTeamID, setStepStatus, token]);
|
|
372
707
|
|
|
373
708
|
const pairBrowser = useCallback(async () => {
|
|
374
709
|
if (!apiUrl || !selectedDevice) return;
|
|
375
710
|
setBusyAction('pair');
|
|
376
711
|
setError(undefined);
|
|
377
712
|
setPairConfirmationRequired(false);
|
|
378
|
-
setCurrentStep('
|
|
379
|
-
setStepStatus('
|
|
713
|
+
setCurrentStep('connect');
|
|
714
|
+
setStepStatus('connect', 'active');
|
|
380
715
|
try {
|
|
381
716
|
await cleanupDeviceAccess();
|
|
382
717
|
const result = await startPairingRelay({
|
|
@@ -392,15 +727,15 @@ export function useDeviceInstall({
|
|
|
392
727
|
await closeDeviceRelayTarget(selectedDevice, log);
|
|
393
728
|
setPairRecord(stored);
|
|
394
729
|
setPairConfirmationRequired(false);
|
|
395
|
-
setStepStatus('
|
|
396
|
-
setCurrentStep('
|
|
730
|
+
setStepStatus('connect', 'complete');
|
|
731
|
+
setCurrentStep('build');
|
|
397
732
|
log('Device paired', 'The pair record was stored locally in this browser.');
|
|
398
733
|
} catch (caught) {
|
|
399
734
|
await closeDeviceRelayTarget(selectedDevice, log);
|
|
400
735
|
const message = errorMessage(caught);
|
|
401
736
|
setPairConfirmationRequired(true);
|
|
402
737
|
setError('Unlock the iPhone, tap Trust, then confirm the pair record.');
|
|
403
|
-
setStepStatus('
|
|
738
|
+
setStepStatus('connect', 'error');
|
|
404
739
|
log('Device pairing failed', message);
|
|
405
740
|
} finally {
|
|
406
741
|
setBusyAction(undefined);
|
|
@@ -446,26 +781,53 @@ export function useDeviceInstall({
|
|
|
446
781
|
device: selectedDevice?.hello,
|
|
447
782
|
hasPairRecord: !!pairRecord,
|
|
448
783
|
hasSigningAssets: !!signingAssets,
|
|
784
|
+
hasSigningInputs: signingInputsReady,
|
|
449
785
|
pairConfirmationRequired,
|
|
450
786
|
logs,
|
|
451
787
|
buildLogs,
|
|
452
788
|
buildStatus,
|
|
453
789
|
appleSigningStatus,
|
|
454
790
|
appleTeams,
|
|
791
|
+
appleDevices,
|
|
792
|
+
appleAppIDs,
|
|
793
|
+
applePortalSummary,
|
|
455
794
|
selectedAppleTeamID,
|
|
795
|
+
selectedAppleDeviceIDs,
|
|
796
|
+
connectedAppleDeviceRegistered,
|
|
797
|
+
connectedDeviceInProfile,
|
|
798
|
+
hasReusableAppleCertificate: !!reusableAppleCertificate,
|
|
799
|
+
appleBundleID,
|
|
456
800
|
buildLogPanelOpen,
|
|
457
801
|
busyAction,
|
|
458
802
|
error,
|
|
459
|
-
canBuild:
|
|
460
|
-
|
|
803
|
+
canBuild:
|
|
804
|
+
!!apiUrl &&
|
|
805
|
+
!busyAction &&
|
|
806
|
+
!!selectedDevice &&
|
|
807
|
+
!!pairRecord &&
|
|
808
|
+
signingInputsReady &&
|
|
809
|
+
connectedDeviceInProfile !== false,
|
|
810
|
+
canPrepareAppleSigningAssets:
|
|
811
|
+
!!apiUrl &&
|
|
812
|
+
!busyAction &&
|
|
813
|
+
!!appleIDLoginRef.current &&
|
|
814
|
+
!!appleBundleID.trim() &&
|
|
815
|
+
!!selectedDeveloperTeamID() &&
|
|
816
|
+
selectedAppleDeviceIDs.length > 0 &&
|
|
817
|
+
(!!reusableAppleCertificate || !!signingFiles.certificatePassword),
|
|
818
|
+
canRequestUSBAccess: !busyAction && signingInputsReady,
|
|
461
819
|
canPairBrowser: !!apiUrl && !busyAction && !!selectedDevice,
|
|
462
820
|
canInstall: !!apiUrl && !busyAction && !!selectedDevice && !!pairRecord,
|
|
463
821
|
setSigningFiles,
|
|
822
|
+
setAppleBundleID,
|
|
823
|
+
setSelectedAppleDeviceIDs,
|
|
464
824
|
setBuildLogPanelOpen,
|
|
465
825
|
startAppleIDLogin,
|
|
466
826
|
submitAppleTwoFactorCode,
|
|
467
|
-
setSelectedAppleTeamID,
|
|
468
|
-
|
|
827
|
+
setSelectedAppleTeamID: selectAppleTeam,
|
|
828
|
+
clearAppleIDLogin,
|
|
829
|
+
registerConnectedAppleDevice,
|
|
830
|
+
prepareAppleSigningAssets,
|
|
469
831
|
startDeviceBuild,
|
|
470
832
|
requestUSBAccess,
|
|
471
833
|
pairBrowser,
|
|
@@ -478,35 +840,361 @@ async function fetchStoredBuildInfo(apiUrl: string, token?: string) {
|
|
|
478
840
|
return fetchLimbuildInfo(apiUrl, token);
|
|
479
841
|
}
|
|
480
842
|
|
|
843
|
+
async function findReusableAppleCertificate(teamID: string): Promise<ReusableAppleCertificate | undefined> {
|
|
844
|
+
const stored = await getLatestSigningAssetsWithCertificate(teamID);
|
|
845
|
+
if (!stored?.certificateID || !stored.certificateP12Base64 || !stored.certificatePassword) {
|
|
846
|
+
return undefined;
|
|
847
|
+
}
|
|
848
|
+
return {
|
|
849
|
+
certificateID: stored.certificateID,
|
|
850
|
+
certificateP12Base64: stored.certificateP12Base64,
|
|
851
|
+
certificatePassword: stored.certificatePassword,
|
|
852
|
+
teamID: stored.teamID,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
481
856
|
async function refreshAppleTeams(
|
|
482
857
|
apiUrl: string,
|
|
483
858
|
appleSessionId: string,
|
|
484
859
|
token: string | undefined,
|
|
485
860
|
setAppleTeams: (teams: AppleDeveloperPortalTeam[]) => void,
|
|
486
861
|
setSelectedAppleTeamID: (teamID: string | undefined) => void,
|
|
862
|
+
accountSession?: AppleDeveloperPortalResponse,
|
|
487
863
|
) {
|
|
488
|
-
const response = await proxyProvisioningRequest<
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
provider?: AppleDeveloperPortalTeam;
|
|
492
|
-
}>(apiUrl, appleSessionId, listTeamsRequest(), token);
|
|
493
|
-
const teams = [
|
|
864
|
+
const response = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(apiUrl, appleSessionId, listTeamsRequest(), token);
|
|
865
|
+
assertApplePortalResponseOK(response.body, 'Apple Developer team list');
|
|
866
|
+
const teams = uniqueAppleTeams([
|
|
494
867
|
...(response.body?.teams ?? []),
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
];
|
|
868
|
+
]);
|
|
869
|
+
void accountSession;
|
|
498
870
|
setAppleTeams(teams);
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
871
|
+
const firstDeveloperTeam = teams.find((team) => developerPortalTeamID(team));
|
|
872
|
+
const firstSelectionID = appleTeamSelectionID(firstDeveloperTeam ?? teams[0]);
|
|
873
|
+
if (firstSelectionID) {
|
|
874
|
+
setSelectedAppleTeamID(firstSelectionID);
|
|
875
|
+
}
|
|
876
|
+
if (teams.length === 0) {
|
|
877
|
+
throw new Error('Apple Developer account did not return any teams or providers.');
|
|
878
|
+
}
|
|
879
|
+
return teams.map(developerPortalTeamID).find((teamID) => !!teamID);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function uniqueAppleTeams(teams: AppleDeveloperPortalTeam[]) {
|
|
883
|
+
const seen = new Set<string>();
|
|
884
|
+
const result: AppleDeveloperPortalTeam[] = [];
|
|
885
|
+
for (const team of teams) {
|
|
886
|
+
const id = appleTeamSelectionID(team);
|
|
887
|
+
const key = id ?? team.name ?? JSON.stringify(team);
|
|
888
|
+
if (seen.has(key)) continue;
|
|
889
|
+
seen.add(key);
|
|
890
|
+
result.push(team);
|
|
891
|
+
}
|
|
892
|
+
return result;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
async function refreshAppleAppIDs(
|
|
896
|
+
apiUrl: string,
|
|
897
|
+
appleSessionId: string,
|
|
898
|
+
token: string | undefined,
|
|
899
|
+
teamID: string | undefined,
|
|
900
|
+
setAppleAppIDs: (appIDs: AppleDeveloperPortalAppID[]) => void,
|
|
901
|
+
setAppleBundleID: (bundleID: string) => void,
|
|
902
|
+
) {
|
|
903
|
+
if (!teamID) return;
|
|
904
|
+
const response = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
905
|
+
apiUrl,
|
|
906
|
+
appleSessionId,
|
|
907
|
+
findBundleIDRequest({ bundleID: '', teamID }),
|
|
908
|
+
token,
|
|
909
|
+
);
|
|
910
|
+
assertApplePortalResponseOK(response.body, 'Apple bundle ID list');
|
|
911
|
+
const appIDs = response.body?.appIds ?? [];
|
|
912
|
+
setAppleAppIDs(appIDs);
|
|
913
|
+
const firstBundleID = bundleIDFromAppleAppID(appIDs[0]);
|
|
914
|
+
if (firstBundleID) {
|
|
915
|
+
setAppleBundleID(firstBundleID);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const info = await fetchStoredBuildInfo(apiUrl, token).catch(() => undefined);
|
|
919
|
+
if (info?.lastBuildConfig?.bundleId) {
|
|
920
|
+
setAppleBundleID(info.lastBuildConfig.bundleId);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async function refreshApplePortalSummary(
|
|
925
|
+
apiUrl: string,
|
|
926
|
+
appleSessionId: string,
|
|
927
|
+
token: string | undefined,
|
|
928
|
+
teamID: string | undefined,
|
|
929
|
+
setApplePortalSummary: (summary: ApplePortalSummary | undefined) => void,
|
|
930
|
+
log: (message: string, detail?: string) => void,
|
|
931
|
+
) {
|
|
932
|
+
const [certificates, profiles] = await Promise.all([
|
|
933
|
+
proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
934
|
+
apiUrl,
|
|
935
|
+
appleSessionId,
|
|
936
|
+
findDevelopmentCertificatesRequest(teamID ?? ''),
|
|
937
|
+
token,
|
|
938
|
+
),
|
|
939
|
+
proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
940
|
+
apiUrl,
|
|
941
|
+
appleSessionId,
|
|
942
|
+
findDevelopmentProfilesRequest({ bundleID: '', teamID: teamID ?? '' }),
|
|
943
|
+
token,
|
|
944
|
+
),
|
|
945
|
+
]);
|
|
946
|
+
assertApplePortalResponseOK(certificates.body, 'Apple Developer certificate list');
|
|
947
|
+
assertApplePortalResponseOK(profiles.body, 'Apple Developer profile list');
|
|
948
|
+
const summary = {
|
|
949
|
+
certificateCount: certificates.body?.certRequests?.length ?? 0,
|
|
950
|
+
profileCount: profiles.body?.provisioningProfiles?.length ?? 0,
|
|
951
|
+
};
|
|
952
|
+
setApplePortalSummary(summary);
|
|
953
|
+
log(
|
|
954
|
+
'Apple Developer resources fetched',
|
|
955
|
+
`${summary.certificateCount} certificates, ${summary.profileCount} provisioning profiles`,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async function refreshAppleDevices({
|
|
960
|
+
apiUrl,
|
|
961
|
+
token,
|
|
962
|
+
appleSessionId,
|
|
963
|
+
teamID,
|
|
964
|
+
connectedUDID,
|
|
965
|
+
setAppleDevices,
|
|
966
|
+
setSelectedAppleDeviceIDs,
|
|
967
|
+
log,
|
|
968
|
+
}: {
|
|
969
|
+
apiUrl: string;
|
|
970
|
+
token?: string;
|
|
971
|
+
appleSessionId: string;
|
|
972
|
+
teamID: string;
|
|
973
|
+
connectedUDID?: string;
|
|
974
|
+
setAppleDevices: (devices: AppleDeveloperPortalDevice[]) => void;
|
|
975
|
+
setSelectedAppleDeviceIDs: (deviceIDs: string[]) => void;
|
|
976
|
+
log: (message: string, detail?: string) => void;
|
|
977
|
+
}) {
|
|
978
|
+
const response = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
979
|
+
apiUrl,
|
|
980
|
+
appleSessionId,
|
|
981
|
+
findDeviceRequest({ deviceUDID: connectedUDID ?? '', teamID }),
|
|
982
|
+
token,
|
|
983
|
+
);
|
|
984
|
+
assertApplePortalResponseOK(response.body, 'Apple device list');
|
|
985
|
+
const devices = response.body?.devices ?? [];
|
|
986
|
+
setAppleDevices(devices);
|
|
987
|
+
const firstDeviceID = devices.find((device) => !!device.deviceId)?.deviceId;
|
|
988
|
+
if (!connectedUDID) {
|
|
989
|
+
setSelectedAppleDeviceIDs(firstDeviceID ? [firstDeviceID] : []);
|
|
990
|
+
log('Apple Developer devices fetched', `${devices.length} devices`);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const connected = devices.find((device) => normalizeAppleUDID(device.deviceNumber) === normalizeAppleUDID(connectedUDID));
|
|
994
|
+
if (connected?.deviceId) {
|
|
995
|
+
setSelectedAppleDeviceIDs([connected.deviceId]);
|
|
996
|
+
log('Connected iPhone found in Apple Developer devices', connected.name ?? connected.deviceNumber);
|
|
997
|
+
} else {
|
|
998
|
+
setSelectedAppleDeviceIDs(firstDeviceID ? [firstDeviceID] : []);
|
|
999
|
+
log('Connected iPhone is not registered with Apple Developer', connectedUDID);
|
|
502
1000
|
}
|
|
503
1001
|
}
|
|
504
1002
|
|
|
505
|
-
function
|
|
1003
|
+
async function prepareAppleSigningAssetsForDevice({
|
|
1004
|
+
apiUrl,
|
|
1005
|
+
token,
|
|
1006
|
+
appleSessionId,
|
|
1007
|
+
teamID,
|
|
1008
|
+
bundleID,
|
|
1009
|
+
deviceUDID,
|
|
1010
|
+
certificatePassword,
|
|
1011
|
+
deviceIDs,
|
|
1012
|
+
reusableCertificate,
|
|
1013
|
+
}: {
|
|
1014
|
+
apiUrl: string;
|
|
1015
|
+
token?: string;
|
|
1016
|
+
appleSessionId: string;
|
|
1017
|
+
teamID: string;
|
|
1018
|
+
bundleID: string;
|
|
1019
|
+
deviceUDID: string;
|
|
1020
|
+
deviceIDs: string[];
|
|
1021
|
+
certificatePassword?: string;
|
|
1022
|
+
reusableCertificate?: ReusableAppleCertificate;
|
|
1023
|
+
}) {
|
|
1024
|
+
const normalizedUDID = deviceUDID.replace(/-/g, '').replace(/[^a-fA-F0-9]/g, '');
|
|
1025
|
+
const appIDID = await findOrCreateAppleBundleID({
|
|
1026
|
+
apiUrl,
|
|
1027
|
+
token,
|
|
1028
|
+
appleSessionId,
|
|
1029
|
+
teamID,
|
|
1030
|
+
bundleID,
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
let certificateID = reusableCertificate?.certificateID;
|
|
1034
|
+
let certificateP12Base64 = reusableCertificate?.certificateP12Base64;
|
|
1035
|
+
let storedCertificatePassword = reusableCertificate?.certificatePassword;
|
|
1036
|
+
if (!certificateID || !certificateP12Base64 || !storedCertificatePassword) {
|
|
1037
|
+
if (!certificatePassword) {
|
|
1038
|
+
throw new Error('Enter a .p12 password before preparing signing assets.');
|
|
1039
|
+
}
|
|
1040
|
+
const keyMaterial = await generateAppleSigningKeyAndCSR({
|
|
1041
|
+
commonName: `Limrun ${bundleID}`,
|
|
1042
|
+
});
|
|
1043
|
+
const certificateResponse = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
1044
|
+
apiUrl,
|
|
1045
|
+
appleSessionId,
|
|
1046
|
+
submitDevelopmentCSRRequest({ csrPEM: keyMaterial.csrPEM, teamID }),
|
|
1047
|
+
token,
|
|
1048
|
+
);
|
|
1049
|
+
assertApplePortalResponseOK(certificateResponse.body, 'Apple Development certificate creation');
|
|
1050
|
+
certificateID =
|
|
1051
|
+
stringField(certificateResponse.body?.certRequest, 'certificateId') ??
|
|
1052
|
+
stringField(certificateResponse.body?.certRequest, 'certRequestId') ??
|
|
1053
|
+
stringField(certificateResponse.body, 'certificateId') ??
|
|
1054
|
+
stringField(certificateResponse.body, 'certRequestId');
|
|
1055
|
+
if (!certificateID) {
|
|
1056
|
+
throw new Error('Apple certificate creation did not return a certificate ID.');
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const downloadedCertificate = await proxyProvisioningRequest(
|
|
1060
|
+
apiUrl,
|
|
1061
|
+
appleSessionId,
|
|
1062
|
+
downloadCertificateRequest(certificateID, teamID),
|
|
1063
|
+
token,
|
|
1064
|
+
);
|
|
1065
|
+
if (downloadedCertificate.status < 200 || downloadedCertificate.status >= 300 || !downloadedCertificate.rawBodyBase64) {
|
|
1066
|
+
throw new Error(`Apple certificate download failed: HTTP ${downloadedCertificate.status}`);
|
|
1067
|
+
}
|
|
1068
|
+
certificateP12Base64 = exportAppleCertificateP12({
|
|
1069
|
+
privateKeyPKCS8Base64: keyMaterial.privateKeyPKCS8Base64,
|
|
1070
|
+
certificateBase64: downloadedCertificate.rawBodyBase64,
|
|
1071
|
+
password: certificatePassword,
|
|
1072
|
+
friendlyName: `Apple Development ${bundleID}`,
|
|
1073
|
+
});
|
|
1074
|
+
storedCertificatePassword = certificatePassword;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const profileName = `Limrun ${bundleID}`;
|
|
1078
|
+
const profileResponse = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
1079
|
+
apiUrl,
|
|
1080
|
+
appleSessionId,
|
|
1081
|
+
createDevelopmentProfileRequest({
|
|
1082
|
+
bundleID,
|
|
1083
|
+
teamID,
|
|
1084
|
+
appIDID,
|
|
1085
|
+
certificateID,
|
|
1086
|
+
deviceIDs,
|
|
1087
|
+
name: profileName,
|
|
1088
|
+
}),
|
|
1089
|
+
token,
|
|
1090
|
+
);
|
|
1091
|
+
assertApplePortalResponseOK(profileResponse.body, 'Apple provisioning profile creation');
|
|
1092
|
+
const profileID =
|
|
1093
|
+
stringField(profileResponse.body?.provisioningProfile, 'provisioningProfileId') ??
|
|
1094
|
+
stringField(profileResponse.body, 'provisioningProfileId');
|
|
1095
|
+
if (!profileID) {
|
|
1096
|
+
throw new Error('Apple provisioning profile creation did not return a profile ID.');
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const downloadedProfile = await proxyProvisioningRequest(
|
|
1100
|
+
apiUrl,
|
|
1101
|
+
appleSessionId,
|
|
1102
|
+
downloadProfileRequest(profileID, teamID),
|
|
1103
|
+
token,
|
|
1104
|
+
);
|
|
1105
|
+
if (downloadedProfile.status < 200 || downloadedProfile.status >= 300 || !downloadedProfile.rawBodyBase64) {
|
|
1106
|
+
throw new Error(`Apple provisioning profile download failed: HTTP ${downloadedProfile.status}`);
|
|
1107
|
+
}
|
|
1108
|
+
const provisioningProfileBase64 = downloadedProfile.rawBodyBase64;
|
|
1109
|
+
const profile = parseProvisioningProfileBase64(provisioningProfileBase64);
|
|
1110
|
+
|
|
1111
|
+
return putAppleGeneratedSigningAssets({
|
|
1112
|
+
bundleID,
|
|
1113
|
+
deviceUDID: normalizedUDID,
|
|
1114
|
+
teamID,
|
|
1115
|
+
certificateID,
|
|
1116
|
+
certificateP12Base64,
|
|
1117
|
+
certificatePassword: storedCertificatePassword,
|
|
1118
|
+
provisioningProfileBase64,
|
|
1119
|
+
profile,
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async function findOrCreateAppleBundleID({
|
|
1124
|
+
apiUrl,
|
|
1125
|
+
token,
|
|
1126
|
+
appleSessionId,
|
|
1127
|
+
teamID,
|
|
1128
|
+
bundleID,
|
|
1129
|
+
}: {
|
|
1130
|
+
apiUrl: string;
|
|
1131
|
+
token?: string;
|
|
1132
|
+
appleSessionId: string;
|
|
1133
|
+
teamID: string;
|
|
1134
|
+
bundleID: string;
|
|
1135
|
+
}) {
|
|
1136
|
+
const existing = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
1137
|
+
apiUrl,
|
|
1138
|
+
appleSessionId,
|
|
1139
|
+
findBundleIDRequest({ bundleID, teamID }),
|
|
1140
|
+
token,
|
|
1141
|
+
);
|
|
1142
|
+
assertApplePortalResponseOK(existing.body, 'Apple bundle ID lookup');
|
|
1143
|
+
const found = existing.body?.appIds?.find((app) => stringField(app, 'identifier') === bundleID || stringField(app, 'bundleId') === bundleID);
|
|
1144
|
+
const foundID = stringField(found, 'appIdId') ?? stringField(found, 'appId');
|
|
1145
|
+
if (foundID) return foundID;
|
|
1146
|
+
|
|
1147
|
+
const created = await proxyProvisioningRequest<AppleDeveloperPortalResponse>(
|
|
1148
|
+
apiUrl,
|
|
1149
|
+
appleSessionId,
|
|
1150
|
+
createBundleIDRequest({ bundleID, teamID, name: bundleID }),
|
|
1151
|
+
token,
|
|
1152
|
+
);
|
|
1153
|
+
assertApplePortalResponseOK(created.body, 'Apple bundle ID creation');
|
|
1154
|
+
const createdID =
|
|
1155
|
+
stringField(created.body?.appId, 'appIdId') ??
|
|
1156
|
+
stringField(created.body?.appId, 'appId') ??
|
|
1157
|
+
stringField(created.body, 'appIdId') ??
|
|
1158
|
+
stringField(created.body, 'appId');
|
|
1159
|
+
if (!createdID) {
|
|
1160
|
+
throw new Error('Apple bundle ID creation did not return an App ID.');
|
|
1161
|
+
}
|
|
1162
|
+
return createdID;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function assertApplePortalResponseOK(response: AppleDeveloperPortalResponse | undefined, label: string) {
|
|
1166
|
+
if (!response) {
|
|
1167
|
+
throw new Error(`${label} returned an empty response.`);
|
|
1168
|
+
}
|
|
1169
|
+
if (response.resultCode !== undefined && response.resultCode !== 0) {
|
|
1170
|
+
throw new Error(`${label} failed: ${response.userString ?? response.resultString ?? response.resultCode}`);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function stringField(record: Record<string, unknown> | undefined, key: string) {
|
|
1175
|
+
const value = record?.[key];
|
|
1176
|
+
if (typeof value === 'string') return value;
|
|
1177
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
1178
|
+
return undefined;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function normalizeAppleUDID(udid?: string) {
|
|
1182
|
+
return (udid ?? '').replace(/-/g, '').replace(/[^a-fA-F0-9]/g, '').toUpperCase();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function appleTeamSelectionID(team?: AppleDeveloperPortalTeam) {
|
|
506
1186
|
const value = team?.teamId ?? team?.providerId ?? team?.publicProviderId;
|
|
507
1187
|
return value === undefined || value === '' ? undefined : String(value);
|
|
508
1188
|
}
|
|
509
1189
|
|
|
1190
|
+
function developerPortalTeamID(team?: AppleDeveloperPortalTeam) {
|
|
1191
|
+
return team?.teamId && team.teamId !== '' ? team.teamId : undefined;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function bundleIDFromAppleAppID(appID?: AppleDeveloperPortalAppID) {
|
|
1195
|
+
return appID?.identifier || appID?.bundleId || undefined;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
510
1198
|
async function fileToBase64(file: File) {
|
|
511
1199
|
const buffer = await file.arrayBuffer();
|
|
512
1200
|
let binary = '';
|