@limrun/ui 0.14.0-rc.5 → 0.14.0
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/README.md +9 -26
- package/dist/components/remote-control.d.ts +36 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1189 -1089
- package/package.json +10 -51
- package/src/components/remote-control.tsx +267 -0
- package/src/index.ts +1 -1
- package/vite.config.ts +0 -9
- package/dist/app-store-relay/index.cjs +0 -9
- package/dist/app-store-relay/index.d.ts +0 -204
- package/dist/app-store-relay/index.js +0 -677
- package/dist/app-store-relay/react.cjs +0 -1
- package/dist/app-store-relay/react.d.ts +0 -15
- package/dist/app-store-relay/react.js +0 -69
- package/dist/apple-credentials/index.cjs +0 -1
- package/dist/apple-credentials/index.d.ts +0 -110
- package/dist/apple-credentials/index.js +0 -291
- package/dist/browser-storage-BnO-Ma-e.mjs +0 -219
- package/dist/browser-storage-DLxIqZk5.js +0 -1
- package/dist/client-C9z_sgOf.js +0 -1
- package/dist/client-CFDB4mX5.mjs +0 -191
- package/dist/core/device-install/apple/client.d.ts +0 -18
- package/dist/core/device-install/apple/crypto.d.ts +0 -20
- package/dist/core/device-install/apple/gsa-srp.d.ts +0 -26
- package/dist/core/device-install/apple/index.d.ts +0 -5
- package/dist/core/device-install/apple/provisioning.d.ts +0 -237
- package/dist/core/device-install/apple/relay.d.ts +0 -53
- package/dist/core/device-install/index.d.ts +0 -4
- package/dist/core/device-install/operations/index.d.ts +0 -6
- package/dist/core/device-install/operations/limbuild-client.d.ts +0 -43
- package/dist/core/device-install/operations/operations.d.ts +0 -47
- package/dist/core/device-install/operations/relay-client.d.ts +0 -26
- package/dist/core/device-install/operations/relay-protocol.d.ts +0 -27
- package/dist/core/device-install/operations/usbmux.d.ts +0 -32
- package/dist/core/device-install/operations/webusb.d.ts +0 -21
- package/dist/core/device-install/storage/browser-storage.d.ts +0 -56
- package/dist/core/device-install/storage/index.d.ts +0 -2
- package/dist/core/device-install/storage/secret-store.d.ts +0 -124
- package/dist/core/device-install/types.d.ts +0 -57
- package/dist/core/errors.d.ts +0 -1
- package/dist/core/play-publish/google.d.ts +0 -13
- package/dist/core/play-publish/publish.d.ts +0 -34
- package/dist/device-build/index.cjs +0 -1
- package/dist/device-build/index.d.ts +0 -4
- package/dist/device-build/index.js +0 -85
- package/dist/device-build/react.cjs +0 -1
- package/dist/device-build/react.d.ts +0 -21
- package/dist/device-build/react.js +0 -68
- package/dist/device-build/signing.d.ts +0 -20
- package/dist/device-install/demo/demo.d.ts +0 -0
- package/dist/device-install/index.cjs +0 -1
- package/dist/device-install/index.d.ts +0 -18
- package/dist/device-install/index.js +0 -594
- package/dist/device-install/react.cjs +0 -1
- package/dist/device-install/react.d.ts +0 -24
- package/dist/device-install/react.js +0 -105
- package/dist/errors-BSkqS7U-.js +0 -1
- package/dist/errors-gy8yQJos.mjs +0 -6
- package/dist/gsa-srp-Bme3JLEi.mjs +0 -266
- package/dist/gsa-srp-CCIfUz7Y.js +0 -1
- package/dist/index-ChhtuQ7e.js +0 -22
- package/dist/index-YDu8K9a4.mjs +0 -11279
- package/dist/limbuild-client-BagK_FWE.mjs +0 -83
- package/dist/limbuild-client-CVDwq54z.js +0 -1
- package/dist/play-publish/index.cjs +0 -1
- package/dist/play-publish/index.d.ts +0 -2
- package/dist/play-publish/index.js +0 -109
- package/dist/play-publish/react.cjs +0 -1
- package/dist/play-publish/react.d.ts +0 -31
- package/dist/play-publish/react.js +0 -91
- package/dist/provisioning-CDPvxd1h.mjs +0 -270
- package/dist/provisioning-CbC4h--7.js +0 -1
- package/src/app-store-relay/index.test.ts +0 -60
- package/src/app-store-relay/index.ts +0 -846
- package/src/app-store-relay/react.ts +0 -131
- package/src/apple-credentials/index.test.ts +0 -180
- package/src/apple-credentials/index.ts +0 -388
- package/src/core/device-install/apple/client.ts +0 -150
- package/src/core/device-install/apple/crypto.ts +0 -205
- package/src/core/device-install/apple/gsa-srp.ts +0 -123
- package/src/core/device-install/apple/index.ts +0 -5
- package/src/core/device-install/apple/provisioning.test.ts +0 -114
- package/src/core/device-install/apple/provisioning.ts +0 -419
- package/src/core/device-install/apple/relay.ts +0 -202
- package/src/core/device-install/index.ts +0 -4
- package/src/core/device-install/operations/index.ts +0 -6
- package/src/core/device-install/operations/limbuild-client.ts +0 -144
- package/src/core/device-install/operations/operations.ts +0 -239
- package/src/core/device-install/operations/relay-client.ts +0 -286
- package/src/core/device-install/operations/relay-protocol.ts +0 -71
- package/src/core/device-install/operations/usbmux.ts +0 -270
- package/src/core/device-install/operations/webusb-dom.d.ts +0 -54
- package/src/core/device-install/operations/webusb.ts +0 -110
- package/src/core/device-install/storage/browser-storage.test.ts +0 -65
- package/src/core/device-install/storage/browser-storage.ts +0 -325
- package/src/core/device-install/storage/index.ts +0 -2
- package/src/core/device-install/storage/secret-store.ts +0 -332
- package/src/core/device-install/types.ts +0 -69
- package/src/core/errors.ts +0 -3
- package/src/core/play-publish/google.ts +0 -113
- package/src/core/play-publish/publish.test.ts +0 -68
- package/src/core/play-publish/publish.ts +0 -89
- package/src/device-build/index.ts +0 -42
- package/src/device-build/react.ts +0 -130
- package/src/device-build/signing.ts +0 -94
- package/src/device-install/README.md +0 -423
- package/src/device-install/demo/demo.css +0 -493
- package/src/device-install/demo/demo.tsx +0 -1061
- package/src/device-install/demo/index.html +0 -12
- package/src/device-install/index.ts +0 -50
- package/src/device-install/react.ts +0 -195
- package/src/play-publish/README.md +0 -66
- package/src/play-publish/index.ts +0 -12
- package/src/play-publish/react.ts +0 -208
|
@@ -1,1061 +0,0 @@
|
|
|
1
|
-
import { StrictMode, useEffect, useMemo, useState, type ChangeEvent, type ReactNode } from 'react';
|
|
2
|
-
import { createRoot } from 'react-dom/client';
|
|
3
|
-
import {
|
|
4
|
-
createAppleBundleID,
|
|
5
|
-
createAppleCertificate,
|
|
6
|
-
createAppleProfile,
|
|
7
|
-
downloadAppleCertificate,
|
|
8
|
-
downloadAppleProfile,
|
|
9
|
-
exportAppleCertificateP12,
|
|
10
|
-
generateAppleSigningKeyAndCSR,
|
|
11
|
-
listAppleBundleIDs,
|
|
12
|
-
listAppleCertificates,
|
|
13
|
-
listAppleDevices,
|
|
14
|
-
listAppleProfiles,
|
|
15
|
-
listAppleTeams,
|
|
16
|
-
registerAppleDevice,
|
|
17
|
-
type AppleDeveloperPortalAppID,
|
|
18
|
-
type AppleDeveloperPortalDevice,
|
|
19
|
-
type AppleDeveloperPortalTeam,
|
|
20
|
-
} from '../../app-store-relay';
|
|
21
|
-
import { useAppleIDLogin } from '../../app-store-relay/react';
|
|
22
|
-
import {
|
|
23
|
-
getLatestSigningAssetsWithCertificate,
|
|
24
|
-
importSigningAssetsFromFiles,
|
|
25
|
-
parseProvisioningProfileBase64,
|
|
26
|
-
putAppleGeneratedSigningAssets,
|
|
27
|
-
type StoredSigningAssets,
|
|
28
|
-
} from '../../device-build';
|
|
29
|
-
import { useDeviceBuild } from '../../device-build/react';
|
|
30
|
-
import { useDeviceInstallRelay } from '../react';
|
|
31
|
-
import './demo.css';
|
|
32
|
-
import { errorMessage } from '../../core/errors';
|
|
33
|
-
|
|
34
|
-
type ActivityLine = {
|
|
35
|
-
id: number;
|
|
36
|
-
time: string;
|
|
37
|
-
message: string;
|
|
38
|
-
detail?: string;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
type StepState = 'pending' | 'active' | 'done' | 'error';
|
|
42
|
-
type StepView = { state: StepState; label: string };
|
|
43
|
-
type PillTone = 'neutral' | 'active' | 'success' | 'danger';
|
|
44
|
-
|
|
45
|
-
type SigningSource = 'apple' | 'upload';
|
|
46
|
-
type CertificateChoice = 'stored' | 'create';
|
|
47
|
-
type ProfileChoice = 'create' | 'existing';
|
|
48
|
-
|
|
49
|
-
type AppleResourceState = {
|
|
50
|
-
teams: AppleDeveloperPortalTeam[];
|
|
51
|
-
appIds: AppleDeveloperPortalAppID[];
|
|
52
|
-
devices: AppleDeveloperPortalDevice[];
|
|
53
|
-
certificates: Array<Record<string, unknown>>;
|
|
54
|
-
profiles: Array<Record<string, unknown>>;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const emptyAppleResources: AppleResourceState = {
|
|
58
|
-
teams: [],
|
|
59
|
-
appIds: [],
|
|
60
|
-
devices: [],
|
|
61
|
-
certificates: [],
|
|
62
|
-
profiles: [],
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const storageKeys = {
|
|
66
|
-
apiUrl: 'limrun-device-demo-api-url',
|
|
67
|
-
token: 'limrun-device-demo-token',
|
|
68
|
-
ipaDownloadUrl: 'limrun-device-demo-ipa-download-url',
|
|
69
|
-
bundleId: 'limrun-device-demo-bundle-id',
|
|
70
|
-
certificatePassword: 'limrun-device-demo-certificate-password',
|
|
71
|
-
appleAccount: 'limrun-device-demo-apple-account',
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
function App() {
|
|
75
|
-
const [apiUrl, setApiUrl] = useLocalStorage(storageKeys.apiUrl, 'http://127.0.0.1:8080');
|
|
76
|
-
const [token, setToken] = useLocalStorage(storageKeys.token, '');
|
|
77
|
-
const [ipaDownloadUrl, setIPADownloadUrl] = useLocalStorage(storageKeys.ipaDownloadUrl, '');
|
|
78
|
-
const [bundleId, setBundleId] = useLocalStorage(storageKeys.bundleId, '');
|
|
79
|
-
const [appleAccount, setAppleAccount] = useLocalStorage(storageKeys.appleAccount, '');
|
|
80
|
-
const [certificatePassword, setCertificatePassword] = useLocalStorage(storageKeys.certificatePassword, '');
|
|
81
|
-
const [signingSource, setSigningSource] = useState<SigningSource>('apple');
|
|
82
|
-
const [applePassword, setApplePassword] = useState('');
|
|
83
|
-
const [twoFactorCode, setTwoFactorCode] = useState('');
|
|
84
|
-
const [resources, setResources] = useState<AppleResourceState>(emptyAppleResources);
|
|
85
|
-
const [selectedTeamId, setSelectedTeamId] = useState('');
|
|
86
|
-
const [selectedAppIdId, setSelectedAppIdId] = useState('');
|
|
87
|
-
const [selectedDeviceIds, setSelectedDeviceIds] = useState<string[]>([]);
|
|
88
|
-
const [certificateChoice, setCertificateChoice] = useState<CertificateChoice>('create');
|
|
89
|
-
const [profileChoice, setProfileChoice] = useState<ProfileChoice>('create');
|
|
90
|
-
const [selectedProfileId, setSelectedProfileId] = useState('');
|
|
91
|
-
const [storedCertificate, setStoredCertificate] = useState<StoredSigningAssets>();
|
|
92
|
-
const [appleBusy, setAppleBusy] = useState<string>();
|
|
93
|
-
const [certificateFile, setCertificateFile] = useState<File>();
|
|
94
|
-
const [profileFile, setProfileFile] = useState<File>();
|
|
95
|
-
const [signingAssets, setSigningAssets] = useState<StoredSigningAssets>();
|
|
96
|
-
const [activity, setActivity] = useState<ActivityLine[]>([]);
|
|
97
|
-
const [prepareError, setPrepareError] = useState<string>();
|
|
98
|
-
const [dismissedError, setDismissedError] = useState<string>();
|
|
99
|
-
const [installPhase, setInstallPhase] = useState<'idle' | 'installing' | 'done' | 'error'>('idle');
|
|
100
|
-
|
|
101
|
-
const addActivity = (message: string, detail?: string) => {
|
|
102
|
-
setActivity((current) =>
|
|
103
|
-
[
|
|
104
|
-
{
|
|
105
|
-
id: Date.now() + Math.random(),
|
|
106
|
-
time: new Date().toLocaleTimeString(),
|
|
107
|
-
message,
|
|
108
|
-
detail,
|
|
109
|
-
},
|
|
110
|
-
...current,
|
|
111
|
-
].slice(0, 120),
|
|
112
|
-
);
|
|
113
|
-
// Derive install completion from relay progress messages (the hook has no
|
|
114
|
-
// explicit "installed" signal). Only act while an install is in flight so
|
|
115
|
-
// pairing's "completed" messages don't flip this.
|
|
116
|
-
setInstallPhase((phase) => {
|
|
117
|
-
if (phase !== 'installing') return phase;
|
|
118
|
-
if (/100% complete/i.test(message) || /install completed/i.test(message)) return 'done';
|
|
119
|
-
if (/^server error/i.test(message) || /install failed/i.test(message)) return 'error';
|
|
120
|
-
return phase;
|
|
121
|
-
});
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const install = useDeviceInstallRelay({
|
|
125
|
-
apiUrl: apiUrl.trim() || undefined,
|
|
126
|
-
token: token.trim() || undefined,
|
|
127
|
-
log: addActivity,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const build = useDeviceBuild({
|
|
131
|
-
apiUrl: apiUrl.trim() || undefined,
|
|
132
|
-
token: token.trim() || undefined,
|
|
133
|
-
signingAssets,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const appleLogin = useAppleIDLogin({
|
|
137
|
-
registryApiUrl: apiUrl.trim(),
|
|
138
|
-
token: token.trim() || undefined,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const combinedError = prepareError ?? appleLogin.error ?? install.error ?? build.error;
|
|
142
|
-
// Show the error as a floating toast (so it's visible without scrolling up)
|
|
143
|
-
// until it's dismissed; a different error re-shows it.
|
|
144
|
-
const visibleError = combinedError && combinedError !== dismissedError ? combinedError : undefined;
|
|
145
|
-
const selectedUDID = install.device?.hello.serialNumber;
|
|
146
|
-
const selectedTeam = resources.teams.find((team) => appleTeamSelectionId(team) === selectedTeamId);
|
|
147
|
-
// Resolve the team id the same way the <select> value is computed, so teams
|
|
148
|
-
// whose portal id lives in providerId/publicProviderId (not teamId) still
|
|
149
|
-
// drive the Apple Developer flow instead of leaving developerTeamId undefined.
|
|
150
|
-
const developerTeamId = appleTeamSelectionId(selectedTeam);
|
|
151
|
-
const selectedProfile = resources.profiles.find(
|
|
152
|
-
(profile) => stringField(profile, 'provisioningProfileId') === selectedProfileId,
|
|
153
|
-
);
|
|
154
|
-
const canUseApple = !!apiUrl.trim() && !!appleLogin.session?.relay && !!developerTeamId;
|
|
155
|
-
const canPrepareSigning = !!certificateFile && !!profileFile;
|
|
156
|
-
const canStartBuild = !!signingAssets && build.status !== 'queued' && build.status !== 'running';
|
|
157
|
-
const canInstall = install.canInstall && build.status === 'succeeded' && !!ipaDownloadUrl.trim();
|
|
158
|
-
|
|
159
|
-
// Live per-step status used by the overview stepper and the section pills.
|
|
160
|
-
const pairStep: StepView =
|
|
161
|
-
install.busyAction === 'usb' ? { state: 'active', label: 'Selecting device…' }
|
|
162
|
-
: install.busyAction === 'pair' ? { state: 'active', label: 'Pairing…' }
|
|
163
|
-
: install.hasPairRecord ? { state: 'done', label: 'Paired' }
|
|
164
|
-
: install.device ? { state: 'active', label: 'Device selected' }
|
|
165
|
-
: { state: 'pending', label: 'Not started' };
|
|
166
|
-
const signStep: StepView =
|
|
167
|
-
signingAssets ? { state: 'done', label: 'Assets ready' }
|
|
168
|
-
: appleBusy === 'signing' ? { state: 'active', label: 'Preparing assets…' }
|
|
169
|
-
: appleBusy === 'login' || appleLogin.status === 'authenticating' ?
|
|
170
|
-
{ state: 'active', label: 'Signing in…' }
|
|
171
|
-
: appleLogin.status === 'two-factor-required' ? { state: 'active', label: '2FA required' }
|
|
172
|
-
: { state: 'pending', label: 'Missing' };
|
|
173
|
-
const buildStep: StepView =
|
|
174
|
-
build.status === 'succeeded' ? { state: 'done', label: 'Succeeded' }
|
|
175
|
-
: build.status === 'failed' || build.status === 'cancelled' ? { state: 'error', label: build.status }
|
|
176
|
-
: build.status === 'queued' || build.status === 'running' ? { state: 'active', label: build.status }
|
|
177
|
-
: { state: 'pending', label: 'Idle' };
|
|
178
|
-
const installStep: StepView =
|
|
179
|
-
installPhase === 'done' ? { state: 'done', label: 'Installed' }
|
|
180
|
-
: installPhase === 'error' ? { state: 'error', label: 'Failed' }
|
|
181
|
-
: install.busyAction === 'install' || installPhase === 'installing' ?
|
|
182
|
-
{ state: 'active', label: 'Installing…' }
|
|
183
|
-
: canInstall ? { state: 'active', label: 'Ready to install' }
|
|
184
|
-
: { state: 'pending', label: 'Waiting' };
|
|
185
|
-
|
|
186
|
-
async function runInstall() {
|
|
187
|
-
setInstallPhase('installing');
|
|
188
|
-
const relay = await install.startInstallation({ downloadUrl: ipaDownloadUrl.trim() });
|
|
189
|
-
if (!relay) setInstallPhase('error');
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const buildLogText = useMemo(
|
|
193
|
-
() =>
|
|
194
|
-
build.logs
|
|
195
|
-
.slice(-80)
|
|
196
|
-
.map((line) => line.data)
|
|
197
|
-
.join('\n'),
|
|
198
|
-
[build.logs],
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
let cancelled = false;
|
|
203
|
-
if (!developerTeamId) {
|
|
204
|
-
setStoredCertificate(undefined);
|
|
205
|
-
setCertificateChoice('create');
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
void getLatestSigningAssetsWithCertificate(developerTeamId, 'development').then((assets) => {
|
|
209
|
-
if (cancelled) return;
|
|
210
|
-
setStoredCertificate(assets);
|
|
211
|
-
setCertificateChoice(assets ? 'stored' : 'create');
|
|
212
|
-
});
|
|
213
|
-
return () => {
|
|
214
|
-
cancelled = true;
|
|
215
|
-
};
|
|
216
|
-
}, [developerTeamId]);
|
|
217
|
-
|
|
218
|
-
async function prepareSigningAssets() {
|
|
219
|
-
if (!certificateFile || !profileFile) return;
|
|
220
|
-
setPrepareError(undefined);
|
|
221
|
-
try {
|
|
222
|
-
const assets = await importSigningAssetsFromFiles({
|
|
223
|
-
certificateFile,
|
|
224
|
-
provisioningProfileFile: profileFile,
|
|
225
|
-
certificatePassword,
|
|
226
|
-
bundleId: bundleId.trim() || undefined,
|
|
227
|
-
deviceUDID: selectedUDID,
|
|
228
|
-
signingMode: 'development',
|
|
229
|
-
});
|
|
230
|
-
setSigningAssets(assets);
|
|
231
|
-
if (!bundleId.trim()) {
|
|
232
|
-
setBundleId(assets.bundleID);
|
|
233
|
-
}
|
|
234
|
-
addActivity('Signing assets ready', assets.bundleID);
|
|
235
|
-
} catch (error) {
|
|
236
|
-
setSigningAssets(undefined);
|
|
237
|
-
setPrepareError(errorMessage(error));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function signInWithApple() {
|
|
242
|
-
if (!apiUrl.trim()) return;
|
|
243
|
-
setAppleBusy('login');
|
|
244
|
-
setPrepareError(undefined);
|
|
245
|
-
try {
|
|
246
|
-
const session = await appleLogin.startLogin({
|
|
247
|
-
accountName: appleAccount,
|
|
248
|
-
password: applePassword,
|
|
249
|
-
});
|
|
250
|
-
setApplePassword('');
|
|
251
|
-
if (session && !session.requiresTwoFactor) {
|
|
252
|
-
await loadAppleTeams(session.relay);
|
|
253
|
-
}
|
|
254
|
-
} catch (error) {
|
|
255
|
-
setPrepareError(errorMessage(error));
|
|
256
|
-
} finally {
|
|
257
|
-
setAppleBusy(undefined);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async function submitAppleTwoFactor() {
|
|
262
|
-
if (!appleLogin.session) return;
|
|
263
|
-
setAppleBusy('2fa');
|
|
264
|
-
setPrepareError(undefined);
|
|
265
|
-
try {
|
|
266
|
-
await appleLogin.submitTwoFactorCode(twoFactorCode);
|
|
267
|
-
setTwoFactorCode('');
|
|
268
|
-
await loadAppleTeams(appleLogin.session.relay);
|
|
269
|
-
} catch (error) {
|
|
270
|
-
setPrepareError(errorMessage(error));
|
|
271
|
-
} finally {
|
|
272
|
-
setAppleBusy(undefined);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async function loadAppleTeams(relay = appleLogin.session?.relay) {
|
|
277
|
-
if (!apiUrl.trim() || !relay) return;
|
|
278
|
-
setAppleBusy('teams');
|
|
279
|
-
setPrepareError(undefined);
|
|
280
|
-
try {
|
|
281
|
-
await appleLogin.finalize().catch(() => undefined);
|
|
282
|
-
const teams = await listAppleTeams({ relay });
|
|
283
|
-
setResources((current) => ({ ...current, teams }));
|
|
284
|
-
const firstTeamId = teams.map(appleTeamSelectionId).find(Boolean) ?? '';
|
|
285
|
-
setSelectedTeamId(firstTeamId);
|
|
286
|
-
const firstDeveloperTeamId = appleTeamSelectionId(
|
|
287
|
-
teams.find((team) => appleTeamSelectionId(team) === firstTeamId),
|
|
288
|
-
);
|
|
289
|
-
if (firstDeveloperTeamId) {
|
|
290
|
-
await loadAppleResources(firstDeveloperTeamId, relay);
|
|
291
|
-
}
|
|
292
|
-
} catch (error) {
|
|
293
|
-
setPrepareError(errorMessage(error));
|
|
294
|
-
} finally {
|
|
295
|
-
setAppleBusy(undefined);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async function loadAppleResources(teamId = developerTeamId, relay = appleLogin.session?.relay) {
|
|
300
|
-
if (!apiUrl.trim() || !relay || !teamId) return;
|
|
301
|
-
const base = {
|
|
302
|
-
relay,
|
|
303
|
-
teamId,
|
|
304
|
-
};
|
|
305
|
-
const [appIds, devices, certificates, profiles] = await Promise.all([
|
|
306
|
-
listAppleBundleIDs(base),
|
|
307
|
-
listAppleDevices(base),
|
|
308
|
-
listAppleCertificates({ ...base, certificateKind: 'development' }),
|
|
309
|
-
listAppleProfiles({ ...base, profileKind: 'development' }),
|
|
310
|
-
]);
|
|
311
|
-
setResources((current) => ({ ...current, appIds, devices, certificates, profiles }));
|
|
312
|
-
const firstApp = appIds[0];
|
|
313
|
-
const firstAppId = appIdIdentifier(firstApp) ?? '';
|
|
314
|
-
const firstBundleId = appIdBundleId(firstApp);
|
|
315
|
-
setSelectedAppIdId(firstAppId);
|
|
316
|
-
if (!bundleId.trim() && firstBundleId) setBundleId(firstBundleId);
|
|
317
|
-
const matchingDevice =
|
|
318
|
-
selectedUDID ? devices.find((device) => sameUDID(device.deviceNumber, selectedUDID)) : undefined;
|
|
319
|
-
const firstDeviceId = matchingDevice?.deviceId ?? devices.find((device) => !!device.deviceId)?.deviceId;
|
|
320
|
-
setSelectedDeviceIds(firstDeviceId ? [firstDeviceId] : []);
|
|
321
|
-
setSelectedProfileId(
|
|
322
|
-
profiles.map((profile) => stringField(profile, 'provisioningProfileId')).find(Boolean) ?? '',
|
|
323
|
-
);
|
|
324
|
-
addActivity('Apple Developer resources loaded', `${devices.length} devices, ${profiles.length} profiles`);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async function createBundleIdResource() {
|
|
328
|
-
if (!canUseApple || !bundleId.trim() || !appleLogin.session || !developerTeamId) return;
|
|
329
|
-
setAppleBusy('bundle');
|
|
330
|
-
setPrepareError(undefined);
|
|
331
|
-
try {
|
|
332
|
-
const app = await createAppleBundleID({
|
|
333
|
-
relay: appleLogin.session.relay,
|
|
334
|
-
teamId: developerTeamId,
|
|
335
|
-
bundleId: bundleId.trim(),
|
|
336
|
-
});
|
|
337
|
-
const appIdId = stringField(app, 'appIdId') ?? stringField(app, 'appId');
|
|
338
|
-
if (appIdId) setSelectedAppIdId(appIdId);
|
|
339
|
-
await loadAppleResources(developerTeamId);
|
|
340
|
-
addActivity('Apple bundle ID created', bundleId.trim());
|
|
341
|
-
} catch (error) {
|
|
342
|
-
setPrepareError(errorMessage(error));
|
|
343
|
-
} finally {
|
|
344
|
-
setAppleBusy(undefined);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
async function registerSelectedIPhone() {
|
|
349
|
-
if (!canUseApple || !appleLogin.session || !developerTeamId || !selectedUDID) return;
|
|
350
|
-
setAppleBusy('register-device');
|
|
351
|
-
setPrepareError(undefined);
|
|
352
|
-
try {
|
|
353
|
-
await registerAppleDevice({
|
|
354
|
-
relay: appleLogin.session.relay,
|
|
355
|
-
teamId: developerTeamId,
|
|
356
|
-
deviceUDID: selectedUDID,
|
|
357
|
-
name: install.device?.hello.productName ?? 'Limrun iPhone',
|
|
358
|
-
});
|
|
359
|
-
await loadAppleResources(developerTeamId);
|
|
360
|
-
addActivity('Registered connected iPhone', selectedUDID);
|
|
361
|
-
} catch (error) {
|
|
362
|
-
setPrepareError(errorMessage(error));
|
|
363
|
-
} finally {
|
|
364
|
-
setAppleBusy(undefined);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
async function prepareAppleSigningAssets() {
|
|
369
|
-
if (!canUseApple || !appleLogin.session || !developerTeamId) return;
|
|
370
|
-
if (!bundleId.trim()) {
|
|
371
|
-
setPrepareError('Enter a bundle ID.');
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
if (!selectedAppIdId) {
|
|
375
|
-
setPrepareError('Select or create a Bundle ID resource.');
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
if (selectedDeviceIds.length === 0) {
|
|
379
|
-
setPrepareError('Select at least one Apple Developer device.');
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
setAppleBusy('signing');
|
|
383
|
-
setPrepareError(undefined);
|
|
384
|
-
try {
|
|
385
|
-
const base = {
|
|
386
|
-
relay: appleLogin.session.relay,
|
|
387
|
-
teamId: developerTeamId,
|
|
388
|
-
};
|
|
389
|
-
let certificateId = storedCertificate?.certificateID;
|
|
390
|
-
let certificateP12Base64 = storedCertificate?.certificateP12Base64;
|
|
391
|
-
let storedCertificatePassword = storedCertificate?.certificatePassword;
|
|
392
|
-
|
|
393
|
-
// Reuse the stored private key, but resolve the id the profile API
|
|
394
|
-
// actually expects. createProvisioningProfile needs the certificate's
|
|
395
|
-
// canonical `certificateId`; the stored value may be a `certRequestId`,
|
|
396
|
-
// so match against the live cert list and use the matched certificateId.
|
|
397
|
-
// If the cert is no longer on the team (revoked), regenerate instead.
|
|
398
|
-
let reuseStoredCertificate = false;
|
|
399
|
-
if (certificateChoice === 'stored' && certificateId && certificateP12Base64) {
|
|
400
|
-
const currentCertificates = await listAppleCertificates({
|
|
401
|
-
...base,
|
|
402
|
-
certificateKind: 'development',
|
|
403
|
-
});
|
|
404
|
-
const matched = currentCertificates.find(
|
|
405
|
-
(item) =>
|
|
406
|
-
stringField(item, 'certificateId') === certificateId ||
|
|
407
|
-
stringField(item, 'certRequestId') === certificateId,
|
|
408
|
-
);
|
|
409
|
-
if (matched) {
|
|
410
|
-
certificateId = stringField(matched, 'certificateId') ?? certificateId;
|
|
411
|
-
reuseStoredCertificate = true;
|
|
412
|
-
addActivity('Reusing stored Apple certificate', certificateId);
|
|
413
|
-
} else {
|
|
414
|
-
addActivity('Stored certificate is no longer on the team', 'Generating a new certificate.');
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (!reuseStoredCertificate) {
|
|
419
|
-
const keyMaterial = await generateAppleSigningKeyAndCSR({
|
|
420
|
-
commonName: `Limrun ${bundleId.trim()}`,
|
|
421
|
-
});
|
|
422
|
-
const certificate = await createAppleCertificate({
|
|
423
|
-
...base,
|
|
424
|
-
certificateKind: 'development',
|
|
425
|
-
csrPEM: keyMaterial.csrPEM,
|
|
426
|
-
});
|
|
427
|
-
certificateId =
|
|
428
|
-
stringField(certificate, 'certificateId') ?? stringField(certificate, 'certRequestId');
|
|
429
|
-
if (!certificateId) throw new Error('Apple certificate creation did not return a certificate ID.');
|
|
430
|
-
const downloadedCertificate = await downloadAppleCertificate({
|
|
431
|
-
...base,
|
|
432
|
-
certificateKind: 'development',
|
|
433
|
-
certificateId,
|
|
434
|
-
});
|
|
435
|
-
if (!downloadedCertificate.rawBodyBase64) {
|
|
436
|
-
throw new Error('Apple certificate download did not return certificate bytes.');
|
|
437
|
-
}
|
|
438
|
-
certificateP12Base64 = exportAppleCertificateP12({
|
|
439
|
-
privateKeyPKCS8Base64: keyMaterial.privateKeyPKCS8Base64,
|
|
440
|
-
certificateBase64: downloadedCertificate.rawBodyBase64,
|
|
441
|
-
password: certificatePassword,
|
|
442
|
-
friendlyName: `Apple Development ${bundleId.trim()}`,
|
|
443
|
-
});
|
|
444
|
-
storedCertificatePassword = certificatePassword || undefined;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
if (certificateChoice === 'stored' && !reuseStoredCertificate) {
|
|
448
|
-
throw new Error(
|
|
449
|
-
'The stored certificate is no longer current on this team (it may have been revoked). ' +
|
|
450
|
-
'Choose "Create new certificate" (needs a free slot in Apple\'s 2-cert limit), or use the ' +
|
|
451
|
-
'"Upload files" tab with a .p12.',
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
if (!certificateId || !certificateP12Base64) {
|
|
455
|
-
throw new Error('Select a stored certificate or create a new certificate.');
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
let profileId = selectedProfileId;
|
|
459
|
-
if (profileChoice === 'create') {
|
|
460
|
-
const profile = await createAppleProfile({
|
|
461
|
-
...base,
|
|
462
|
-
profileKind: 'development',
|
|
463
|
-
bundleId: bundleId.trim(),
|
|
464
|
-
appIdId: selectedAppIdId,
|
|
465
|
-
certificateIds: [certificateId],
|
|
466
|
-
deviceIds: selectedDeviceIds,
|
|
467
|
-
name: `Limrun ${bundleId.trim()} ${Date.now()}`,
|
|
468
|
-
});
|
|
469
|
-
profileId = stringField(profile, 'provisioningProfileId') ?? stringField(profile, 'profileId') ?? '';
|
|
470
|
-
}
|
|
471
|
-
if (!profileId) throw new Error('Select or create a provisioning profile.');
|
|
472
|
-
const downloadedProfile = await downloadAppleProfile({ ...base, profileId });
|
|
473
|
-
if (!downloadedProfile.rawBodyBase64) {
|
|
474
|
-
throw new Error('Apple provisioning profile download did not return profile bytes.');
|
|
475
|
-
}
|
|
476
|
-
const profile = parseProvisioningProfileBase64(downloadedProfile.rawBodyBase64);
|
|
477
|
-
const assets = await putAppleGeneratedSigningAssets({
|
|
478
|
-
bundleID: bundleId.trim(),
|
|
479
|
-
deviceUDID: selectedUDID,
|
|
480
|
-
teamID: developerTeamId,
|
|
481
|
-
signingMode: 'development',
|
|
482
|
-
certificateID: certificateId,
|
|
483
|
-
certificateP12Base64,
|
|
484
|
-
certificatePassword: storedCertificatePassword,
|
|
485
|
-
provisioningProfileBase64: downloadedProfile.rawBodyBase64,
|
|
486
|
-
profile,
|
|
487
|
-
});
|
|
488
|
-
setSigningAssets(assets);
|
|
489
|
-
await loadAppleResources(developerTeamId);
|
|
490
|
-
addActivity('Apple signing assets ready', assets.bundleID);
|
|
491
|
-
} catch (error) {
|
|
492
|
-
setSigningAssets(undefined);
|
|
493
|
-
const message = errorMessage(error);
|
|
494
|
-
const capHit =
|
|
495
|
-
/current Development certificate|pending certificate request|Maximum number of certificates/i.test(
|
|
496
|
-
message,
|
|
497
|
-
);
|
|
498
|
-
setPrepareError(
|
|
499
|
-
capHit ?
|
|
500
|
-
`${message}\n\nApple caps development certificates at 2. Either pick "Use stored local certificate" (if this browser already created one), upload a .p12 instead, or revoke an existing certificate at developer.apple.com and retry.`
|
|
501
|
-
: message,
|
|
502
|
-
);
|
|
503
|
-
} finally {
|
|
504
|
-
setAppleBusy(undefined);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
async function startBuild() {
|
|
509
|
-
const execId = await build.startBuild({ signingAssets });
|
|
510
|
-
if (execId) {
|
|
511
|
-
addActivity('Signed device build started', execId);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
return (
|
|
516
|
-
<main className="page">
|
|
517
|
-
<header className="hero">
|
|
518
|
-
<p className="eyebrow">Limrun WebUSB Demo</p>
|
|
519
|
-
<h1>Install a signed iOS build onto a physical iPhone</h1>
|
|
520
|
-
<p>
|
|
521
|
-
This page demonstrates the upload-signing path from
|
|
522
|
-
<code> REAL_DEVICE_INSTALL_README.md</code>: select an iPhone, pair it, upload signing assets, start
|
|
523
|
-
a signed <code>iphoneos</code> build, and install over WebUSB.
|
|
524
|
-
</p>
|
|
525
|
-
</header>
|
|
526
|
-
|
|
527
|
-
<section className="stepper">
|
|
528
|
-
{[
|
|
529
|
-
{ n: 1, title: 'Pair', step: pairStep },
|
|
530
|
-
{ n: 2, title: 'Sign', step: signStep },
|
|
531
|
-
{ n: 3, title: 'Build', step: buildStep },
|
|
532
|
-
{ n: 4, title: 'Install', step: installStep },
|
|
533
|
-
].map(({ n, title, step }) => (
|
|
534
|
-
<div key={n} className={`step ${step.state}`}>
|
|
535
|
-
<span className="stepDot">
|
|
536
|
-
{step.state === 'done' ?
|
|
537
|
-
'✓'
|
|
538
|
-
: step.state === 'error' ?
|
|
539
|
-
'!'
|
|
540
|
-
: n}
|
|
541
|
-
</span>
|
|
542
|
-
<div className="stepText">
|
|
543
|
-
<span className="stepTitle">{title}</span>
|
|
544
|
-
<span className="stepLabel">{step.label}</span>
|
|
545
|
-
</div>
|
|
546
|
-
</div>
|
|
547
|
-
))}
|
|
548
|
-
</section>
|
|
549
|
-
|
|
550
|
-
{visibleError && (
|
|
551
|
-
<div className="errorToast" role="alert">
|
|
552
|
-
<div className="errorToastBody">
|
|
553
|
-
<strong>Something went wrong</strong>
|
|
554
|
-
<pre>{visibleError}</pre>
|
|
555
|
-
</div>
|
|
556
|
-
<button
|
|
557
|
-
type="button"
|
|
558
|
-
className="errorToastClose"
|
|
559
|
-
aria-label="Dismiss"
|
|
560
|
-
onClick={() => setDismissedError(combinedError)}
|
|
561
|
-
>
|
|
562
|
-
✕
|
|
563
|
-
</button>
|
|
564
|
-
</div>
|
|
565
|
-
)}
|
|
566
|
-
|
|
567
|
-
<section className="card">
|
|
568
|
-
<h2>Connection</h2>
|
|
569
|
-
<div className="grid two">
|
|
570
|
-
<label>
|
|
571
|
-
limbuild API URL
|
|
572
|
-
<input
|
|
573
|
-
value={apiUrl}
|
|
574
|
-
onChange={(event) => setApiUrl(event.currentTarget.value)}
|
|
575
|
-
placeholder="http://127.0.0.1:8080"
|
|
576
|
-
/>
|
|
577
|
-
</label>
|
|
578
|
-
<label>
|
|
579
|
-
Token (optional)
|
|
580
|
-
<input
|
|
581
|
-
value={token}
|
|
582
|
-
onChange={(event) => setToken(event.currentTarget.value)}
|
|
583
|
-
placeholder="Instance token, if required"
|
|
584
|
-
/>
|
|
585
|
-
</label>
|
|
586
|
-
</div>
|
|
587
|
-
<p className="hint">
|
|
588
|
-
Run this page on <code>localhost</code> or HTTPS. WebUSB is available in Chromium browsers only.
|
|
589
|
-
</p>
|
|
590
|
-
<label>
|
|
591
|
-
IPA download URL
|
|
592
|
-
<input
|
|
593
|
-
value={ipaDownloadUrl}
|
|
594
|
-
onChange={(event) => setIPADownloadUrl(event.currentTarget.value)}
|
|
595
|
-
placeholder="https://asset-storage.example/app.ipa"
|
|
596
|
-
/>
|
|
597
|
-
</label>
|
|
598
|
-
</section>
|
|
599
|
-
|
|
600
|
-
<section className="card">
|
|
601
|
-
<div className="sectionHeader">
|
|
602
|
-
<div>
|
|
603
|
-
<h2>1. Pair iPhone</h2>
|
|
604
|
-
<p>Select the USB device, then pair. Unlock the iPhone and tap Trust.</p>
|
|
605
|
-
</div>
|
|
606
|
-
<StatusPill tone={pillTone(pairStep.state)}>{pairStep.label}</StatusPill>
|
|
607
|
-
</div>
|
|
608
|
-
<div className="actions">
|
|
609
|
-
<button
|
|
610
|
-
type="button"
|
|
611
|
-
disabled={!apiUrl.trim() || !!install.busyAction}
|
|
612
|
-
onClick={() => void install.requestUSBAccess()}
|
|
613
|
-
>
|
|
614
|
-
Select iPhone
|
|
615
|
-
</button>
|
|
616
|
-
<button type="button" disabled={!install.canPair} onClick={() => void install.pairBrowser()}>
|
|
617
|
-
Pair
|
|
618
|
-
</button>
|
|
619
|
-
<button type="button" className="secondary" onClick={install.stopRelay}>
|
|
620
|
-
Stop relay
|
|
621
|
-
</button>
|
|
622
|
-
</div>
|
|
623
|
-
<dl className="facts">
|
|
624
|
-
<div>
|
|
625
|
-
<dt>Device</dt>
|
|
626
|
-
<dd>{install.device?.hello.productName ?? 'Not selected'}</dd>
|
|
627
|
-
</div>
|
|
628
|
-
<div>
|
|
629
|
-
<dt>UDID</dt>
|
|
630
|
-
<dd>{selectedUDID ?? 'Not selected'}</dd>
|
|
631
|
-
</div>
|
|
632
|
-
<div>
|
|
633
|
-
<dt>Busy action</dt>
|
|
634
|
-
<dd>{install.busyAction ?? 'idle'}</dd>
|
|
635
|
-
</div>
|
|
636
|
-
</dl>
|
|
637
|
-
</section>
|
|
638
|
-
|
|
639
|
-
<section className="card">
|
|
640
|
-
<div className="sectionHeader">
|
|
641
|
-
<div>
|
|
642
|
-
<h2>2. Prepare signing assets</h2>
|
|
643
|
-
<p>
|
|
644
|
-
Use the Apple Developer relay or upload a <code>.p12</code> and
|
|
645
|
-
<code> .mobileprovision</code> directly.
|
|
646
|
-
</p>
|
|
647
|
-
</div>
|
|
648
|
-
<StatusPill tone={pillTone(signStep.state)}>{signStep.label}</StatusPill>
|
|
649
|
-
</div>
|
|
650
|
-
|
|
651
|
-
<div className="tabs" role="tablist" aria-label="Signing source">
|
|
652
|
-
<button
|
|
653
|
-
type="button"
|
|
654
|
-
className={signingSource === 'apple' ? 'tab active' : 'tab'}
|
|
655
|
-
onClick={() => setSigningSource('apple')}
|
|
656
|
-
>
|
|
657
|
-
Apple Developer
|
|
658
|
-
</button>
|
|
659
|
-
<button
|
|
660
|
-
type="button"
|
|
661
|
-
className={signingSource === 'upload' ? 'tab active' : 'tab'}
|
|
662
|
-
onClick={() => setSigningSource('upload')}
|
|
663
|
-
>
|
|
664
|
-
Upload files
|
|
665
|
-
</button>
|
|
666
|
-
</div>
|
|
667
|
-
|
|
668
|
-
<div className="grid two">
|
|
669
|
-
<label>
|
|
670
|
-
Bundle ID
|
|
671
|
-
<input
|
|
672
|
-
value={bundleId}
|
|
673
|
-
onChange={(event) => setBundleId(event.currentTarget.value)}
|
|
674
|
-
placeholder="com.example.MyApp"
|
|
675
|
-
/>
|
|
676
|
-
</label>
|
|
677
|
-
<label>
|
|
678
|
-
Certificate password
|
|
679
|
-
<input
|
|
680
|
-
type="password"
|
|
681
|
-
value={certificatePassword}
|
|
682
|
-
onChange={(event) => setCertificatePassword(event.currentTarget.value)}
|
|
683
|
-
/>
|
|
684
|
-
</label>
|
|
685
|
-
</div>
|
|
686
|
-
|
|
687
|
-
{signingSource === 'apple' ?
|
|
688
|
-
<div className="subPanel">
|
|
689
|
-
<div className="sectionHeader compact">
|
|
690
|
-
<div>
|
|
691
|
-
<h3>Apple ID</h3>
|
|
692
|
-
<p>Status: {appleLogin.status}</p>
|
|
693
|
-
</div>
|
|
694
|
-
<button
|
|
695
|
-
type="button"
|
|
696
|
-
disabled={!appleAccount || !applePassword || appleBusy === 'login'}
|
|
697
|
-
onClick={() => void signInWithApple()}
|
|
698
|
-
>
|
|
699
|
-
Sign in
|
|
700
|
-
</button>
|
|
701
|
-
</div>
|
|
702
|
-
<div className="grid two">
|
|
703
|
-
<label>
|
|
704
|
-
Apple ID
|
|
705
|
-
<input
|
|
706
|
-
type="email"
|
|
707
|
-
autoComplete="username"
|
|
708
|
-
value={appleAccount}
|
|
709
|
-
onChange={(event) => setAppleAccount(event.currentTarget.value)}
|
|
710
|
-
/>
|
|
711
|
-
</label>
|
|
712
|
-
<label>
|
|
713
|
-
Apple ID password
|
|
714
|
-
<input
|
|
715
|
-
type="password"
|
|
716
|
-
autoComplete="current-password"
|
|
717
|
-
value={applePassword}
|
|
718
|
-
onChange={(event) => setApplePassword(event.currentTarget.value)}
|
|
719
|
-
/>
|
|
720
|
-
</label>
|
|
721
|
-
</div>
|
|
722
|
-
{appleLogin.status === 'two-factor-required' && (
|
|
723
|
-
<div className="inlineForm">
|
|
724
|
-
<input
|
|
725
|
-
inputMode="numeric"
|
|
726
|
-
autoComplete="one-time-code"
|
|
727
|
-
placeholder="Two-factor code"
|
|
728
|
-
value={twoFactorCode}
|
|
729
|
-
onChange={(event) => setTwoFactorCode(event.currentTarget.value)}
|
|
730
|
-
/>
|
|
731
|
-
<button
|
|
732
|
-
type="button"
|
|
733
|
-
disabled={!twoFactorCode || appleBusy === '2fa'}
|
|
734
|
-
onClick={() => void submitAppleTwoFactor()}
|
|
735
|
-
>
|
|
736
|
-
Submit code
|
|
737
|
-
</button>
|
|
738
|
-
</div>
|
|
739
|
-
)}
|
|
740
|
-
|
|
741
|
-
<div className="grid two">
|
|
742
|
-
<label>
|
|
743
|
-
Team
|
|
744
|
-
<select
|
|
745
|
-
value={selectedTeamId}
|
|
746
|
-
disabled={resources.teams.length === 0}
|
|
747
|
-
onChange={(event) => {
|
|
748
|
-
const value = event.currentTarget.value;
|
|
749
|
-
setSelectedTeamId(value);
|
|
750
|
-
const team = resources.teams.find((item) => appleTeamSelectionId(item) === value);
|
|
751
|
-
const teamId = appleTeamSelectionId(team);
|
|
752
|
-
if (teamId) void loadAppleResources(teamId);
|
|
753
|
-
}}
|
|
754
|
-
>
|
|
755
|
-
<option value="">Sign in to load teams</option>
|
|
756
|
-
{resources.teams.map((team, index) => {
|
|
757
|
-
const value = appleTeamSelectionId(team) ?? `team-${index}`;
|
|
758
|
-
return (
|
|
759
|
-
<option key={value} value={value}>
|
|
760
|
-
{team.name ?? value}
|
|
761
|
-
</option>
|
|
762
|
-
);
|
|
763
|
-
})}
|
|
764
|
-
</select>
|
|
765
|
-
</label>
|
|
766
|
-
<label>
|
|
767
|
-
Bundle ID resource
|
|
768
|
-
<select
|
|
769
|
-
value={selectedAppIdId}
|
|
770
|
-
disabled={resources.appIds.length === 0}
|
|
771
|
-
onChange={(event) => setSelectedAppIdId(event.currentTarget.value)}
|
|
772
|
-
>
|
|
773
|
-
<option value="">Select or create a Bundle ID resource</option>
|
|
774
|
-
{resources.appIds.map((appId, index) => {
|
|
775
|
-
const value = appIdIdentifier(appId) ?? `app-${index}`;
|
|
776
|
-
return (
|
|
777
|
-
<option key={value} value={value}>
|
|
778
|
-
{appIdBundleId(appId) ?? appId.name ?? value}
|
|
779
|
-
</option>
|
|
780
|
-
);
|
|
781
|
-
})}
|
|
782
|
-
</select>
|
|
783
|
-
</label>
|
|
784
|
-
</div>
|
|
785
|
-
<div className="actions">
|
|
786
|
-
<button
|
|
787
|
-
type="button"
|
|
788
|
-
disabled={!canUseApple || !bundleId.trim() || appleBusy === 'bundle'}
|
|
789
|
-
onClick={() => void createBundleIdResource()}
|
|
790
|
-
>
|
|
791
|
-
Create bundle ID
|
|
792
|
-
</button>
|
|
793
|
-
<button
|
|
794
|
-
type="button"
|
|
795
|
-
className="secondary"
|
|
796
|
-
disabled={!canUseApple || !selectedUDID || appleBusy === 'register-device'}
|
|
797
|
-
onClick={() => void registerSelectedIPhone()}
|
|
798
|
-
>
|
|
799
|
-
Register selected iPhone
|
|
800
|
-
</button>
|
|
801
|
-
<button
|
|
802
|
-
type="button"
|
|
803
|
-
className="secondary"
|
|
804
|
-
disabled={!appleLogin.session || appleBusy === 'teams'}
|
|
805
|
-
onClick={() => void loadAppleTeams()}
|
|
806
|
-
>
|
|
807
|
-
Reload Apple resources
|
|
808
|
-
</button>
|
|
809
|
-
</div>
|
|
810
|
-
<div className="grid two">
|
|
811
|
-
<label>
|
|
812
|
-
Apple devices
|
|
813
|
-
<select
|
|
814
|
-
multiple
|
|
815
|
-
className="multiSelect"
|
|
816
|
-
value={selectedDeviceIds}
|
|
817
|
-
onChange={(event) =>
|
|
818
|
-
setSelectedDeviceIds(
|
|
819
|
-
Array.from(event.currentTarget.selectedOptions).map((option) => option.value),
|
|
820
|
-
)
|
|
821
|
-
}
|
|
822
|
-
>
|
|
823
|
-
{resources.devices.map((device, index) => {
|
|
824
|
-
const value = device.deviceId ?? `device-${index}`;
|
|
825
|
-
return (
|
|
826
|
-
<option key={value} value={value}>
|
|
827
|
-
{device.name ?? device.deviceNumber ?? value}
|
|
828
|
-
</option>
|
|
829
|
-
);
|
|
830
|
-
})}
|
|
831
|
-
</select>
|
|
832
|
-
</label>
|
|
833
|
-
<label>
|
|
834
|
-
Existing profile
|
|
835
|
-
<select
|
|
836
|
-
value={selectedProfileId}
|
|
837
|
-
disabled={resources.profiles.length === 0}
|
|
838
|
-
onChange={(event) => setSelectedProfileId(event.currentTarget.value)}
|
|
839
|
-
>
|
|
840
|
-
<option value="">Select profile</option>
|
|
841
|
-
{resources.profiles.map((profile, index) => {
|
|
842
|
-
const value = stringField(profile, 'provisioningProfileId') ?? `profile-${index}`;
|
|
843
|
-
return (
|
|
844
|
-
<option key={value} value={value}>
|
|
845
|
-
{stringField(profile, 'name') ?? value}
|
|
846
|
-
</option>
|
|
847
|
-
);
|
|
848
|
-
})}
|
|
849
|
-
</select>
|
|
850
|
-
</label>
|
|
851
|
-
<label>
|
|
852
|
-
Certificate
|
|
853
|
-
<select
|
|
854
|
-
value={certificateChoice}
|
|
855
|
-
onChange={(event) => setCertificateChoice(event.currentTarget.value as CertificateChoice)}
|
|
856
|
-
>
|
|
857
|
-
<option value="stored" disabled={!storedCertificate}>
|
|
858
|
-
Use stored local certificate
|
|
859
|
-
</option>
|
|
860
|
-
<option value="create">Create new certificate</option>
|
|
861
|
-
</select>
|
|
862
|
-
</label>
|
|
863
|
-
<label>
|
|
864
|
-
Provisioning profile
|
|
865
|
-
<select
|
|
866
|
-
value={profileChoice}
|
|
867
|
-
onChange={(event) => setProfileChoice(event.currentTarget.value as ProfileChoice)}
|
|
868
|
-
>
|
|
869
|
-
<option value="create">Create new profile</option>
|
|
870
|
-
<option value="existing" disabled={!selectedProfile}>
|
|
871
|
-
Use selected existing profile
|
|
872
|
-
</option>
|
|
873
|
-
</select>
|
|
874
|
-
</label>
|
|
875
|
-
</div>
|
|
876
|
-
<p className="hint">
|
|
877
|
-
Apple account has {resources.certificates.length} matching development certificates. Existing
|
|
878
|
-
Apple certs can only be reused when this browser already has the private key.
|
|
879
|
-
</p>
|
|
880
|
-
<div className="actions">
|
|
881
|
-
<button
|
|
882
|
-
type="button"
|
|
883
|
-
disabled={!canUseApple || appleBusy === 'signing'}
|
|
884
|
-
onClick={() => void prepareAppleSigningAssets()}
|
|
885
|
-
>
|
|
886
|
-
Prepare Apple signing assets
|
|
887
|
-
</button>
|
|
888
|
-
</div>
|
|
889
|
-
</div>
|
|
890
|
-
: <div className="subPanel">
|
|
891
|
-
<p className="hint">
|
|
892
|
-
Use a development <code>.mobileprovision</code> that includes the selected iPhone UDID and
|
|
893
|
-
covers the app bundle ID.
|
|
894
|
-
</p>
|
|
895
|
-
<div className="grid two">
|
|
896
|
-
<label>
|
|
897
|
-
.p12 certificate
|
|
898
|
-
<input
|
|
899
|
-
type="file"
|
|
900
|
-
accept=".p12,application/x-pkcs12"
|
|
901
|
-
onChange={(event) => updateFile(setCertificateFile, event)}
|
|
902
|
-
/>
|
|
903
|
-
</label>
|
|
904
|
-
<label>
|
|
905
|
-
.mobileprovision profile
|
|
906
|
-
<input
|
|
907
|
-
type="file"
|
|
908
|
-
accept=".mobileprovision"
|
|
909
|
-
onChange={(event) => updateFile(setProfileFile, event)}
|
|
910
|
-
/>
|
|
911
|
-
</label>
|
|
912
|
-
</div>
|
|
913
|
-
<div className="actions">
|
|
914
|
-
<button type="button" disabled={!canPrepareSigning} onClick={() => void prepareSigningAssets()}>
|
|
915
|
-
Prepare uploaded signing assets
|
|
916
|
-
</button>
|
|
917
|
-
</div>
|
|
918
|
-
</div>
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
{signingAssets && (
|
|
922
|
-
<dl className="facts">
|
|
923
|
-
<div>
|
|
924
|
-
<dt>Profile bundle</dt>
|
|
925
|
-
<dd>{signingAssets.profile.bundleID ?? signingAssets.bundleID}</dd>
|
|
926
|
-
</div>
|
|
927
|
-
<div>
|
|
928
|
-
<dt>Team</dt>
|
|
929
|
-
<dd>{signingAssets.teamID ?? signingAssets.profile.teamID ?? 'unknown'}</dd>
|
|
930
|
-
</div>
|
|
931
|
-
<div>
|
|
932
|
-
<dt>Devices in profile</dt>
|
|
933
|
-
<dd>{signingAssets.profile.provisionedDevices.length}</dd>
|
|
934
|
-
</div>
|
|
935
|
-
</dl>
|
|
936
|
-
)}
|
|
937
|
-
</section>
|
|
938
|
-
|
|
939
|
-
<section className="card">
|
|
940
|
-
<div className="sectionHeader">
|
|
941
|
-
<div>
|
|
942
|
-
<h2>3. Build</h2>
|
|
943
|
-
<p>Starts a signed device build on limbuild and streams xcodebuild logs.</p>
|
|
944
|
-
</div>
|
|
945
|
-
<StatusPill tone={pillTone(buildStep.state)}>{buildStep.label}</StatusPill>
|
|
946
|
-
</div>
|
|
947
|
-
<div className="actions">
|
|
948
|
-
<button type="button" disabled={!canStartBuild} onClick={() => void startBuild()}>
|
|
949
|
-
Start signed build
|
|
950
|
-
</button>
|
|
951
|
-
</div>
|
|
952
|
-
<pre className="logBox">{buildLogText || 'Build logs will appear here.'}</pre>
|
|
953
|
-
</section>
|
|
954
|
-
|
|
955
|
-
<section className="card">
|
|
956
|
-
<div className="sectionHeader">
|
|
957
|
-
<div>
|
|
958
|
-
<h2>4. Install</h2>
|
|
959
|
-
<p>Installs the latest successful signed build onto the paired iPhone.</p>
|
|
960
|
-
</div>
|
|
961
|
-
<StatusPill tone={pillTone(installStep.state)}>{installStep.label}</StatusPill>
|
|
962
|
-
</div>
|
|
963
|
-
<div className="actions">
|
|
964
|
-
<button
|
|
965
|
-
type="button"
|
|
966
|
-
disabled={!canInstall || installPhase === 'installing'}
|
|
967
|
-
onClick={() => void runInstall()}
|
|
968
|
-
>
|
|
969
|
-
Install to iPhone
|
|
970
|
-
</button>
|
|
971
|
-
</div>
|
|
972
|
-
</section>
|
|
973
|
-
|
|
974
|
-
<section className="card">
|
|
975
|
-
<div className="sectionHeader">
|
|
976
|
-
<div>
|
|
977
|
-
<h2>Activity</h2>
|
|
978
|
-
<p>Device, relay, signing, build, and install events.</p>
|
|
979
|
-
</div>
|
|
980
|
-
<button type="button" className="secondary" onClick={() => setActivity([])}>
|
|
981
|
-
Clear
|
|
982
|
-
</button>
|
|
983
|
-
</div>
|
|
984
|
-
<div className="activity">
|
|
985
|
-
{activity.length === 0 ?
|
|
986
|
-
<p className="hint">No activity yet.</p>
|
|
987
|
-
: activity.map((line) => (
|
|
988
|
-
<div key={line.id} className="activityLine">
|
|
989
|
-
<span>{line.time}</span>
|
|
990
|
-
<strong>{line.message}</strong>
|
|
991
|
-
{line.detail && <pre>{line.detail}</pre>}
|
|
992
|
-
</div>
|
|
993
|
-
))
|
|
994
|
-
}
|
|
995
|
-
</div>
|
|
996
|
-
</section>
|
|
997
|
-
</main>
|
|
998
|
-
);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
function pillTone(state: StepState): PillTone {
|
|
1002
|
-
return (
|
|
1003
|
-
state === 'done' ? 'success'
|
|
1004
|
-
: state === 'error' ? 'danger'
|
|
1005
|
-
: state === 'active' ? 'active'
|
|
1006
|
-
: 'neutral'
|
|
1007
|
-
);
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
function StatusPill({ tone, children }: { tone: PillTone; children: ReactNode }) {
|
|
1011
|
-
return <span className={`pill ${tone}`}>{children}</span>;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
function updateFile(setter: (file: File | undefined) => void, event: ChangeEvent<HTMLInputElement>) {
|
|
1015
|
-
setter(event.currentTarget.files?.[0]);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
function appleTeamSelectionId(team?: AppleDeveloperPortalTeam) {
|
|
1019
|
-
const value = team?.teamId ?? team?.providerId ?? team?.publicProviderId;
|
|
1020
|
-
return value === undefined || value === '' ? undefined : String(value);
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
function appIdIdentifier(appId?: AppleDeveloperPortalAppID) {
|
|
1024
|
-
return appId?.appIdId ?? appId?.appId ?? appId?.identifier ?? appId?.bundleId;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
function appIdBundleId(appId?: AppleDeveloperPortalAppID) {
|
|
1028
|
-
return appId?.identifier ?? appId?.bundleId;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
function stringField(record: Record<string, unknown> | undefined, key: string) {
|
|
1032
|
-
const value = record?.[key];
|
|
1033
|
-
if (typeof value === 'string') return value;
|
|
1034
|
-
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
1035
|
-
return undefined;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
function sameUDID(left?: string, right?: string) {
|
|
1039
|
-
return normalizeUDID(left) === normalizeUDID(right);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
function normalizeUDID(udid?: string) {
|
|
1043
|
-
return (udid ?? '')
|
|
1044
|
-
.replace(/-/g, '')
|
|
1045
|
-
.replace(/[^a-fA-F0-9]/g, '')
|
|
1046
|
-
.toUpperCase();
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
function useLocalStorage(key: string, initialValue: string) {
|
|
1050
|
-
const [value, setValue] = useState(() => localStorage.getItem(key) ?? initialValue);
|
|
1051
|
-
useEffect(() => {
|
|
1052
|
-
localStorage.setItem(key, value);
|
|
1053
|
-
}, [key, value]);
|
|
1054
|
-
return [value, setValue] as const;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
createRoot(document.getElementById('root')!).render(
|
|
1058
|
-
<StrictMode>
|
|
1059
|
-
<App />
|
|
1060
|
-
</StrictMode>,
|
|
1061
|
-
);
|