import { StrictMode, useEffect, useMemo, useState, type ChangeEvent, type ReactNode } from 'react'; import { createRoot } from 'react-dom/client'; import { createAppleBundleID, createAppleCertificate, createAppleProfile, downloadAppleCertificate, downloadAppleProfile, exportAppleCertificateP12, generateAppleSigningKeyAndCSR, listAppleBundleIDs, listAppleCertificates, listAppleDevices, listAppleProfiles, listAppleTeams, registerAppleDevice, type AppleDeveloperPortalAppID, type AppleDeveloperPortalDevice, type AppleDeveloperPortalTeam, } from '../../app-store-relay'; import { useAppleIDLogin } from '../../app-store-relay/react'; import { getLatestSigningAssetsWithCertificate, importSigningAssetsFromFiles, parseProvisioningProfileBase64, putAppleGeneratedSigningAssets, type StoredSigningAssets, } from '../../device-build'; import { useDeviceBuild } from '../../device-build/react'; import { useDeviceInstallRelay } from '../react'; import './demo.css'; type ActivityLine = { id: number; time: string; message: string; detail?: string; }; type StepState = 'pending' | 'active' | 'done' | 'error'; type StepView = { state: StepState; label: string }; type PillTone = 'neutral' | 'active' | 'success' | 'danger'; type SigningSource = 'apple' | 'upload'; type CertificateChoice = 'stored' | 'create'; type ProfileChoice = 'create' | 'existing'; type AppleResourceState = { teams: AppleDeveloperPortalTeam[]; appIds: AppleDeveloperPortalAppID[]; devices: AppleDeveloperPortalDevice[]; certificates: Array>; profiles: Array>; }; const emptyAppleResources: AppleResourceState = { teams: [], appIds: [], devices: [], certificates: [], profiles: [], }; const storageKeys = { apiUrl: 'limrun-device-demo-api-url', token: 'limrun-device-demo-token', bundleId: 'limrun-device-demo-bundle-id', certificatePassword: 'limrun-device-demo-certificate-password', appleAccount: 'limrun-device-demo-apple-account', }; function App() { const [apiUrl, setApiUrl] = useLocalStorage(storageKeys.apiUrl, 'http://127.0.0.1:8080'); const [token, setToken] = useLocalStorage(storageKeys.token, ''); const [bundleId, setBundleId] = useLocalStorage(storageKeys.bundleId, ''); const [appleAccount, setAppleAccount] = useLocalStorage(storageKeys.appleAccount, ''); const [certificatePassword, setCertificatePassword] = useLocalStorage(storageKeys.certificatePassword, ''); const [signingSource, setSigningSource] = useState('apple'); const [applePassword, setApplePassword] = useState(''); const [twoFactorCode, setTwoFactorCode] = useState(''); const [resources, setResources] = useState(emptyAppleResources); const [selectedTeamId, setSelectedTeamId] = useState(''); const [selectedAppIdId, setSelectedAppIdId] = useState(''); const [selectedDeviceIds, setSelectedDeviceIds] = useState([]); const [certificateChoice, setCertificateChoice] = useState('create'); const [profileChoice, setProfileChoice] = useState('create'); const [selectedProfileId, setSelectedProfileId] = useState(''); const [storedCertificate, setStoredCertificate] = useState(); const [appleBusy, setAppleBusy] = useState(); const [certificateFile, setCertificateFile] = useState(); const [profileFile, setProfileFile] = useState(); const [signingAssets, setSigningAssets] = useState(); const [activity, setActivity] = useState([]); const [prepareError, setPrepareError] = useState(); const [dismissedError, setDismissedError] = useState(); const [installPhase, setInstallPhase] = useState<'idle' | 'installing' | 'done' | 'error'>('idle'); const addActivity = (message: string, detail?: string) => { setActivity((current) => [ { id: Date.now() + Math.random(), time: new Date().toLocaleTimeString(), message, detail, }, ...current, ].slice(0, 120), ); // Derive install completion from relay progress messages (the hook has no // explicit "installed" signal). Only act while an install is in flight so // pairing's "completed" messages don't flip this. setInstallPhase((phase) => { if (phase !== 'installing') return phase; if (/100% complete/i.test(message) || /install completed/i.test(message)) return 'done'; if (/^server error/i.test(message) || /install failed/i.test(message)) return 'error'; return phase; }); }; const install = useDeviceInstallRelay({ apiUrl: apiUrl.trim() || undefined, token: token.trim() || undefined, log: addActivity, }); const build = useDeviceBuild({ apiUrl: apiUrl.trim() || undefined, token: token.trim() || undefined, signingAssets, }); const appleLogin = useAppleIDLogin({ limbuildApiUrl: apiUrl.trim(), token: token.trim() || undefined, }); const combinedError = prepareError ?? appleLogin.error ?? install.error ?? build.error; // Show the error as a floating toast (so it's visible without scrolling up) // until it's dismissed; a different error re-shows it. const visibleError = combinedError && combinedError !== dismissedError ? combinedError : undefined; const selectedUDID = install.device?.hello.serialNumber; const selectedTeam = resources.teams.find((team) => appleTeamSelectionId(team) === selectedTeamId); // Resolve the team id the same way the setApiUrl(event.currentTarget.value)} placeholder="http://127.0.0.1:8080" />

Run this page on localhost or HTTPS. WebUSB is available in Chromium browsers only.

1. Pair iPhone

Select the USB device, then pair. Unlock the iPhone and tap Trust.

{pairStep.label}
Device
{install.device?.hello.productName ?? 'Not selected'}
UDID
{selectedUDID ?? 'Not selected'}
Busy action
{install.busyAction ?? 'idle'}

2. Prepare signing assets

Use the Apple Developer relay or upload a .p12 and .mobileprovision directly.

{signStep.label}
{signingSource === 'apple' ?

Apple ID

Status: {appleLogin.status}

{appleLogin.status === 'two-factor-required' && (
setTwoFactorCode(event.currentTarget.value)} />
)}

Apple account has {resources.certificates.length} matching development certificates. Existing Apple certs can only be reused when this browser already has the private key.

:

Use a development .mobileprovision that includes the selected iPhone UDID and covers the app bundle ID.

} {signingAssets && (
Profile bundle
{signingAssets.profile.bundleID ?? signingAssets.bundleID}
Team
{signingAssets.teamID ?? signingAssets.profile.teamID ?? 'unknown'}
Devices in profile
{signingAssets.profile.provisionedDevices.length}
)}

3. Build

Starts a signed device build on limbuild and streams xcodebuild logs.

{buildStep.label}
{buildLogText || 'Build logs will appear here.'}

4. Install

Installs the latest successful signed build onto the paired iPhone.

{installStep.label}

Activity

Device, relay, signing, build, and install events.

{activity.length === 0 ?

No activity yet.

: activity.map((line) => (
{line.time} {line.message} {line.detail &&
{line.detail}
}
)) }
); } function pillTone(state: StepState): PillTone { return ( state === 'done' ? 'success' : state === 'error' ? 'danger' : state === 'active' ? 'active' : 'neutral' ); } function StatusPill({ tone, children }: { tone: PillTone; children: ReactNode }) { return {children}; } function updateFile(setter: (file: File | undefined) => void, event: ChangeEvent) { setter(event.currentTarget.files?.[0]); } function appleTeamSelectionId(team?: AppleDeveloperPortalTeam) { const value = team?.teamId ?? team?.providerId ?? team?.publicProviderId; return value === undefined || value === '' ? undefined : String(value); } function appIdIdentifier(appId?: AppleDeveloperPortalAppID) { return appId?.appIdId ?? appId?.appId ?? appId?.identifier ?? appId?.bundleId; } function appIdBundleId(appId?: AppleDeveloperPortalAppID) { return appId?.identifier ?? appId?.bundleId; } function stringField(record: Record | undefined, key: string) { const value = record?.[key]; if (typeof value === 'string') return value; if (typeof value === 'number' || typeof value === 'boolean') return String(value); return undefined; } function sameUDID(left?: string, right?: string) { return normalizeUDID(left) === normalizeUDID(right); } function normalizeUDID(udid?: string) { return (udid ?? '') .replace(/-/g, '') .replace(/[^a-fA-F0-9]/g, '') .toUpperCase(); } function useLocalStorage(key: string, initialValue: string) { const [value, setValue] = useState(() => localStorage.getItem(key) ?? initialValue); useEffect(() => { localStorage.setItem(key, value); }, [key, value]); return [value, setValue] as const; } function errorMessage(error: unknown) { return error instanceof Error ? error.message : String(error); } createRoot(document.getElementById('root')!).render( , );