diff --git a/app/setup/page.tsx b/app/setup/page.tsx index 1db6ebfd..5e67d8a5 100644 --- a/app/setup/page.tsx +++ b/app/setup/page.tsx @@ -227,7 +227,15 @@ export default function SetupWizardPage() { await saveStep(step, values); setStepIndex((i) => Math.min(i + 1, STEPS.length - 1)); } catch (e) { - setError(humanError(e)); + const msg = humanError(e); + setError(msg); + // Session expired mid-flow — kick the user back to the + // welcome step so they can re-enter the token without + // having to refresh. + if (/wizard session required/i.test(msg)) { + setAuthenticated(false); + setStepIndex(0); + } } }} onBack={() => setStepIndex((i) => Math.max(i - 1, 1))} @@ -392,15 +400,45 @@ function CenteredCard({ children }: { children: ReactNode }) { function ErrorBanner({ error, onDismiss }: { error: string; onDismiss: () => void }) { return ( -
- {error} -
); } +/** + * Translate raw API error strings into user-facing copy. Matches the friendly + * tone of the JMAP probe cards. + */ +function friendlyError(raw: string): string { + const lower = raw.toLowerCase(); + if (lower.includes('invalid or expired token')) { + return "That setup token isn't valid anymore. Restart the container to get a fresh one from the logs."; + } + if (lower.includes('wizard session required')) { + return 'Your wizard session expired. Paste the setup token again to continue.'; + } + if (lower.includes('token required')) { + return 'Paste the setup token printed in the container logs to continue.'; + } + if (lower.includes('setup is not active')) { + return 'Setup has already finished. Reload to sign in.'; + } + return raw; +} + // ─── Welcome / token step ──────────────────────────────────────────────── function WelcomeStep({ tokenFromUrl, onSubmit }: { tokenFromUrl: string; onSubmit: (t: string) => Promise }) {