@carrierllc/mcp 0.13.40 → 0.13.47

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.
Files changed (69) hide show
  1. package/dist/{chunk-WVXII7ZR.js → chunk-4XUAUQ3U.js} +67 -7
  2. package/dist/chunk-4XUAUQ3U.js.map +1 -0
  3. package/dist/cli.js +56 -10
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +71 -17
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/plugin/carrier/skills/wallet/SKILL.md +1 -1
  9. package/templates/storefront/CLERK.md +23 -0
  10. package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +29 -22
  11. package/templates/storefront/src/app/activate/[orderId]/page.tsx +2 -2
  12. package/templates/storefront/src/app/api/checkout/claim/route.ts +7 -6
  13. package/templates/storefront/src/app/api/checkout/guest/route.ts +72 -17
  14. package/templates/storefront/src/app/api/orders/[orderId]/route.ts +2 -2
  15. package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +51 -25
  16. package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +165 -131
  17. package/templates/storefront/src/app/checkout/success/FocusHeading.tsx +20 -0
  18. package/templates/storefront/src/app/checkout/success/page.tsx +136 -15
  19. package/templates/storefront/src/app/dashboard/EsimControls.tsx +20 -24
  20. package/templates/storefront/src/app/dashboard/page.tsx +43 -23
  21. package/templates/storefront/src/app/globals.css +44 -5
  22. package/templates/storefront/src/app/help/page.tsx +2 -2
  23. package/templates/storefront/src/app/layout.tsx +5 -3
  24. package/templates/storefront/src/app/page.tsx +27 -7
  25. package/templates/storefront/src/app/shop/ShopClient.tsx +106 -72
  26. package/templates/storefront/src/app/shop/page.tsx +7 -2
  27. package/templates/storefront/src/app/sign-in/[[...sign-in]]/StorefrontSignInClient.tsx +297 -0
  28. package/templates/storefront/src/app/sign-in/[[...sign-in]]/page.tsx +13 -14
  29. package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +31 -28
  30. package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +5 -2
  31. package/templates/storefront/src/brand.config.ts +16 -0
  32. package/templates/storefront/src/components/Providers.tsx +20 -1
  33. package/templates/storefront/src/components/faq/FaqSection.tsx +2 -2
  34. package/templates/storefront/src/components/footer/StorefrontFooter.tsx +7 -7
  35. package/templates/storefront/src/components/landing/FinalCTA.tsx +4 -9
  36. package/templates/storefront/src/components/landing/HeroSection.tsx +4 -7
  37. package/templates/storefront/src/components/landing/HowItWorks.tsx +1 -1
  38. package/templates/storefront/src/components/landing/PlanCard.tsx +23 -13
  39. package/templates/storefront/src/components/landing/TierPlanBoard.tsx +237 -0
  40. package/templates/storefront/src/components/nav/StorefrontNav.tsx +8 -11
  41. package/templates/storefront/src/components/shop/CountrySearch.tsx +94 -0
  42. package/templates/storefront/src/components/theme/BrandStyles.tsx +2 -2
  43. package/templates/storefront/src/components/theme/ThemeToggle.tsx +1 -1
  44. package/templates/storefront/src/components/theme/brand-accent-css.ts +21 -0
  45. package/templates/storefront/src/components/theme/brand-style.ts +88 -0
  46. package/templates/storefront/src/components/theme/clerk-appearance.ts +18 -2
  47. package/templates/storefront/src/lib/buyer-auth-copy.ts +83 -0
  48. package/templates/storefront/src/lib/buyer-error.ts +32 -0
  49. package/templates/storefront/src/lib/buyer-session.ts +41 -0
  50. package/templates/storefront/src/lib/catalog-countries.ts +179 -0
  51. package/templates/storefront/src/lib/checkout-client-error.ts +28 -0
  52. package/templates/storefront/src/lib/checkout-copy.ts +89 -0
  53. package/templates/storefront/src/lib/checkout-order-claim.ts +8 -7
  54. package/templates/storefront/src/lib/checkout-return.ts +18 -0
  55. package/templates/storefront/src/lib/clerk-instance.ts +89 -0
  56. package/templates/storefront/src/lib/order-reference.ts +15 -0
  57. package/templates/storefront/src/lib/request-origin.ts +28 -0
  58. package/templates/storefront/src/lib/sanitize-auth-redirect.ts +20 -4
  59. package/templates/storefront/src/lib/tap-target.ts +17 -0
  60. package/templates/storefront/src/lib/verify-checkout-session.ts +118 -19
  61. package/templates/storefront/src/middleware.ts +11 -0
  62. package/templates/storefront/src/vendor/carrier/catalog-guard.ts +9 -5
  63. package/templates/storefront/src/vendor/carrier/client.ts +22 -4
  64. package/templates/storefront/src/vendor/carrier/country-offers.ts +106 -0
  65. package/templates/storefront/src/vendor/carrier/stripe-price.ts +134 -0
  66. package/templates/storefront/src/vendor/carrier/tiers.ts +161 -0
  67. package/templates/storefront/src/vendor/carrier/types.ts +22 -0
  68. package/templates/storefront/src/vendor/geo/index.ts +15 -4
  69. package/dist/chunk-WVXII7ZR.js.map +0 -1
@@ -2,18 +2,20 @@
2
2
  /**
3
3
  * CheckoutSuccessClient
4
4
  *
5
- * Shown after Stripe payment success.
5
+ * Shown after a paid Stripe Checkout session for this shop.
6
6
  *
7
- * - If already signed in: redirect immediately to /activate/[orderId].
8
- * - If not signed in: show email + password sign-up form (no phone).
9
- * After sign-up or sign-in, redirect to /activate/[orderId].
7
+ * The confirmation stays on screen for signed-in and signed-out buyers.
8
+ * Creating an account is an inline step so the buyer can install the eSIM;
9
+ * it does not take another payment.
10
10
  *
11
- * The orderId is verified server-side before this component renders.
11
+ * The order reference drawn here is a short tail. The full session id is only
12
+ * used in the activate URL.
12
13
  */
13
- import { useState, FormEvent, useEffect, useCallback } from "react";
14
+ import { useState, FormEvent, useEffect, useCallback, useRef } from "react";
14
15
  import { useSignUp, useSignIn } from "@clerk/nextjs";
15
16
  import { useClerk, useAuth } from "@clerk/nextjs";
16
17
  import { useRouter } from "next/navigation";
18
+ import { accountStepError, buyerFacingError, shortOrderReference } from "@/lib/checkout-copy";
17
19
  import { completeEmailSignUpAfterVerify } from "@/lib/complete-email-sign-up";
18
20
 
19
21
  type AuthMode = "signup" | "signin";
@@ -24,6 +26,65 @@ interface Props {
24
26
  sessionId: string;
25
27
  templateId: string;
26
28
  purchaserEmail: string | null;
29
+ supportHref: string;
30
+ }
31
+
32
+ class ClaimFailure extends Error {
33
+ readonly status: number;
34
+ constructor(status: number) {
35
+ super("claim failed");
36
+ this.name = "ClaimFailure";
37
+ this.status = status;
38
+ }
39
+ }
40
+
41
+ const filledButtonClass =
42
+ "inline-flex min-h-11 w-full items-center justify-center rounded-lg px-4 text-base font-semibold disabled:opacity-50";
43
+ const filledButtonStyle = { background: "var(--brand-accent)", color: "var(--brand-on-accent)" };
44
+ const fieldClass =
45
+ "w-full min-h-11 rounded-lg border border-sand-500 bg-sand-50 px-3 text-base text-sand-900 placeholder:text-sand-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand-accent-text)] dark:bg-sand-950 dark:text-sand-50 dark:placeholder:text-sand-200";
46
+
47
+ function FieldError({ id, message }: { id: string; message: string | null }) {
48
+ if (!message) return null;
49
+ return (
50
+ <p
51
+ id={id}
52
+ role="alert"
53
+ aria-live="polite"
54
+ className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-base text-[#9F1239] dark:text-[#FECDD3]"
55
+ >
56
+ {message}
57
+ </p>
58
+ );
59
+ }
60
+
61
+ function Confirmation({ reference, supportHref }: { reference: string; supportHref: string }) {
62
+ const heading = useRef<HTMLHeadingElement>(null);
63
+ useEffect(() => {
64
+ heading.current?.focus();
65
+ }, []);
66
+ return (
67
+ <div role="status" className="rounded-2xl border border-sand-200 bg-white px-6 py-4 text-center dark:border-sand-800 dark:bg-sand-900">
68
+ <h1 ref={heading} tabIndex={-1} className="text-lg font-semibold text-[#166534] dark:text-[#BBF7D0]">
69
+ Payment confirmed
70
+ </h1>
71
+ <p className="mt-1 text-base text-sand-700 dark:text-sand-200">
72
+ Your payment is complete. No further charge is made on this page.
73
+ </p>
74
+ <p className="mt-2 max-w-full truncate font-mono text-base text-sand-700 dark:text-sand-200">
75
+ Order {reference}
76
+ </p>
77
+ <p className="mt-2 text-base text-sand-700 dark:text-sand-200">
78
+ Next, save this eSIM to an account. We email the QR code once that account is ready.
79
+ </p>
80
+ <a
81
+ href={supportHref}
82
+ className="mt-4 inline-flex min-h-11 items-center justify-center text-base text-[var(--brand-accent-text)]"
83
+ >
84
+ Contact support
85
+ </a>
86
+ </div>
87
+ );
27
88
  }
28
89
 
29
90
  export function CheckoutSuccessClient({
@@ -31,6 +92,7 @@ export function CheckoutSuccessClient({
31
92
  sessionId,
32
93
  templateId,
33
94
  purchaserEmail,
95
+ supportHref,
34
96
  }: Props) {
35
97
  const { isLoaded: authLoaded, isSignedIn } = useAuth();
36
98
  const { signUp, isLoaded: signUpLoaded } = useSignUp();
@@ -38,6 +100,7 @@ export function CheckoutSuccessClient({
38
100
  const { setActive } = useClerk();
39
101
  const router = useRouter();
40
102
 
103
+ const reference = shortOrderReference(orderId);
41
104
  const activateUrl = `/activate/${encodeURIComponent(orderId)}`;
42
105
  const [mode, setMode] = useState<AuthMode>("signup");
43
106
  const [flowStep, setStep] = useState<FlowStep>("idle");
@@ -52,21 +115,25 @@ export function CheckoutSuccessClient({
52
115
  const emailLocked = Boolean(purchaserEmail);
53
116
 
54
117
  const claimOrderAndRedirect = useCallback(async () => {
55
- const res = await fetch("/api/checkout/claim", {
56
- method: "POST",
57
- headers: { "Content-Type": "application/json" },
58
- body: JSON.stringify({ sessionId, templateId }),
59
- });
60
- if (!res.ok) {
61
- const err = (await res.json()) as { error?: string };
62
- throw new Error(err.error ?? "Failed to link your purchase to this account");
118
+ const res = await fetch("/api/checkout/claim", {
119
+ method: "POST",
120
+ headers: { "Content-Type": "application/json" },
121
+ body: JSON.stringify({ sessionId, templateId }),
122
+ });
123
+ if (!res.ok) {
124
+ try {
125
+ await res.json();
126
+ } catch {
127
+ // Unreadable bodies are not buyer copy.
63
128
  }
64
- router.replace(activateUrl);
129
+ throw new ClaimFailure(res.status);
130
+ }
131
+ router.replace(activateUrl);
65
132
  }, [activateUrl, router, sessionId, templateId]);
66
133
 
67
134
  const handleClaimError = useCallback((err: unknown) => {
68
135
  setStep("claim-error");
69
- setError(err instanceof Error ? err.message : "Failed to link your purchase to this account");
136
+ setError(buyerFacingError(err instanceof ClaimFailure ? err.status : 0));
70
137
  }, []);
71
138
 
72
139
  async function startClaimAndRedirect() {
@@ -75,7 +142,6 @@ export function CheckoutSuccessClient({
75
142
  await claimOrderAndRedirect().catch(handleClaimError);
76
143
  }
77
144
 
78
- // If already signed in, claim the paid order before activating.
79
145
  useEffect(() => {
80
146
  if (!authLoaded) return;
81
147
  if (isSignedIn) {
@@ -83,17 +149,6 @@ export function CheckoutSuccessClient({
83
149
  }
84
150
  }, [authLoaded, isSignedIn, claimOrderAndRedirect, handleClaimError]);
85
151
 
86
- function clerkErrMsg(err: unknown): string {
87
- const e = err as { errors?: Array<{ longMessage?: string; message?: string }> };
88
- return (
89
- e?.errors?.[0]?.longMessage ??
90
- e?.errors?.[0]?.message ??
91
- "Something went wrong. Please try again."
92
- );
93
- }
94
-
95
- // ── Sign-up flow ──────────────────────────────────────────────────────────
96
-
97
152
  async function handleSignUp(e: FormEvent<HTMLFormElement>) {
98
153
  e.preventDefault();
99
154
  if (!signUpLoaded || !signUp) return;
@@ -108,7 +163,7 @@ export function CheckoutSuccessClient({
108
163
  await signUp.prepareEmailAddressVerification({ strategy: "email_code" });
109
164
  setStep("verify");
110
165
  } catch (err) {
111
- setError(clerkErrMsg(err));
166
+ setError(accountStepError(err));
112
167
  } finally {
113
168
  setLoading(false);
114
169
  }
@@ -129,14 +184,12 @@ export function CheckoutSuccessClient({
129
184
  setError(completion.message);
130
185
  }
131
186
  } catch (err) {
132
- setError(clerkErrMsg(err));
187
+ setError(accountStepError(err));
133
188
  } finally {
134
189
  setLoading(false);
135
190
  }
136
191
  }
137
192
 
138
- // ── Sign-in flow ──────────────────────────────────────────────────────────
139
-
140
193
  async function handleSignIn(e: FormEvent<HTMLFormElement>) {
141
194
  e.preventDefault();
142
195
  if (!signInLoaded || !signIn) return;
@@ -158,107 +211,86 @@ export function CheckoutSuccessClient({
158
211
  setError("Sign-in incomplete. Please try again.");
159
212
  }
160
213
  } catch (err) {
161
- setError(clerkErrMsg(err));
214
+ setError(accountStepError(err));
162
215
  } finally {
163
216
  setLoading(false);
164
217
  }
165
218
  }
166
219
 
167
- // ── Render ────────────────────────────────────────────────────────────────
220
+ const invalid = error ? true : undefined;
221
+ const showAccount = step === "credentials" || step === "verify";
168
222
 
169
- if (step === "idle" || step === "redirecting") {
170
- return (
171
- <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4">
172
- <div className="text-center space-y-3">
173
- <div className="mx-auto size-8 animate-spin rounded-full border-2 border-[var(--brand-accent)] border-t-transparent" />
174
- <p className="text-sm text-sand-600 dark:text-sand-400">
175
- {step === "redirecting" ? "Activating your eSIM…" : "Loading…"}
176
- </p>
177
- </div>
178
- </div>
179
- );
180
- }
223
+ return (
224
+ <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4 py-16">
225
+ <div className="w-full max-w-md space-y-6">
226
+ <Confirmation reference={reference} supportHref={supportHref} />
181
227
 
182
- if (step === "claim-error") {
183
- return (
184
- <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4 py-16">
185
- <div className="w-full max-w-md space-y-6 text-center">
186
- <div className="rounded-2xl border border-emerald-500/30 bg-emerald-500/10 px-6 py-4">
187
- <p className="text-lg font-semibold text-emerald-300">✓ Payment successful!</p>
228
+ {step === "idle" || step === "redirecting" ? (
229
+ <div className="text-center">
230
+ <div className="mx-auto size-8 animate-spin rounded-full border-2 border-[var(--brand-accent)] border-t-transparent" />
231
+ <p className="mt-3 text-base text-sand-700 dark:text-sand-200">
232
+ {step === "redirecting" ? "Activating your eSIM…" : "Loading…"}
233
+ </p>
188
234
  </div>
189
- <div className="rounded-2xl border border-sand-200 dark:border-sand-800 bg-white dark:bg-sand-900 p-8 backdrop-blur space-y-4">
190
- <p className="text-sm text-sand-700 dark:text-sand-200">We could not link this purchase to your account.</p>
191
- {error && (
192
- <p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
193
- {error}
194
- </p>
195
- )}
235
+ ) : null}
236
+
237
+ {step === "claim-error" ? (
238
+ <div className="space-y-4 rounded-2xl border border-sand-200 bg-white p-8 text-center dark:border-sand-800 dark:bg-sand-900">
239
+ <p className="text-base text-sand-700 dark:text-sand-200">
240
+ We could not link this purchase to your account.
241
+ </p>
242
+ <FieldError id="claim-error" message={error} />
196
243
  <button
197
244
  type="button"
198
245
  onClick={() => void startClaimAndRedirect()}
199
- className="w-full rounded-lg bg-[var(--brand-accent)] py-2 text-sm font-semibold text-white transition-opacity hover:opacity-90"
246
+ className={filledButtonClass}
247
+ style={filledButtonStyle}
200
248
  >
201
249
  Try again
202
250
  </button>
203
251
  </div>
204
- </div>
205
- </div>
206
- );
207
- }
208
-
209
- return (
210
- <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4 py-16">
211
- <div className="w-full max-w-md space-y-6">
212
- {/* Payment success banner */}
213
- <div className="rounded-2xl border border-emerald-500/30 bg-emerald-500/10 px-6 py-4 text-center">
214
- <p className="text-lg font-semibold text-emerald-300">✓ Payment successful!</p>
215
- <p className="mt-1 text-sm text-emerald-200/70">
216
- Create an account to activate your eSIM.
217
- </p>
218
- </div>
252
+ ) : null}
219
253
 
220
- {/* Auth card */}
221
- <div className="rounded-2xl border border-sand-200 dark:border-sand-800 bg-white dark:bg-sand-900 p-8 backdrop-blur">
222
- {/* Mode toggle */}
254
+ {showAccount ? (
255
+ <div className="rounded-2xl border border-sand-200 bg-white p-8 dark:border-sand-800 dark:bg-sand-900">
223
256
  {step === "credentials" && (
224
- <div className="flex rounded-lg border border-sand-200 dark:border-sand-800 p-1 mb-6">
257
+ <div className="mb-6 flex rounded-lg border border-sand-200 p-1 dark:border-sand-800">
225
258
  <button
226
259
  type="button"
260
+ aria-pressed={mode === "signup"}
227
261
  onClick={() => { setMode("signup"); setError(null); }}
228
- className={`flex-1 rounded-md py-1.5 text-sm font-medium transition-colors ${
229
- mode === "signup"
230
- ? "bg-[var(--brand-accent)] text-sand-900 dark:text-sand-50"
231
- : "text-sand-500 hover:text-sand-600 dark:text-sand-300"
262
+ className={`inline-flex min-h-11 flex-1 items-center justify-center rounded-md text-base font-medium ${
263
+ mode === "signup" ? "" : "text-sand-700 dark:text-sand-200"
232
264
  }`}
265
+ style={mode === "signup" ? filledButtonStyle : undefined}
233
266
  >
234
267
  Create account
235
268
  </button>
236
269
  <button
237
270
  type="button"
271
+ aria-pressed={mode === "signin"}
238
272
  onClick={() => { setMode("signin"); setError(null); }}
239
- className={`flex-1 rounded-md py-1.5 text-sm font-medium transition-colors ${
240
- mode === "signin"
241
- ? "bg-[var(--brand-accent)] text-sand-900 dark:text-sand-50"
242
- : "text-sand-500 hover:text-sand-600 dark:text-sand-300"
273
+ className={`inline-flex min-h-11 flex-1 items-center justify-center rounded-md text-base font-medium ${
274
+ mode === "signin" ? "" : "text-sand-700 dark:text-sand-200"
243
275
  }`}
276
+ style={mode === "signin" ? filledButtonStyle : undefined}
244
277
  >
245
278
  Sign in
246
279
  </button>
247
280
  </div>
248
281
  )}
249
282
 
250
- {/* Sign-up: credentials step */}
251
283
  {step === "credentials" && mode === "signup" && (
252
284
  <>
253
- <h2 className="text-base font-semibold text-sand-900 dark:text-sand-50 mb-1">Create your account</h2>
254
- <p className="text-xs text-sand-500 mb-5">
285
+ <h2 className="mb-1 text-base font-semibold text-sand-900 dark:text-sand-50">Create your account</h2>
286
+ <p className="mb-5 text-base text-sand-700 dark:text-sand-200">
255
287
  {emailLocked
256
288
  ? "Use the email from your checkout to create your account."
257
- : "Email and password — no phone required."}
289
+ : "Email and password. No phone required."}
258
290
  </p>
259
291
  <form onSubmit={handleSignUp} className="space-y-4">
260
292
  <div>
261
- <label className="block text-xs font-medium text-sand-600 dark:text-sand-400 mb-1" htmlFor="su-email">
293
+ <label className="mb-1 block text-base font-medium text-sand-700 dark:text-sand-200" htmlFor="su-email">
262
294
  Email address
263
295
  </label>
264
296
  <input
@@ -269,12 +301,14 @@ export function CheckoutSuccessClient({
269
301
  readOnly={emailLocked}
270
302
  value={email}
271
303
  onChange={(e) => setEmail(e.target.value)}
272
- className="w-full rounded-lg border border-sand-200 dark:border-sand-800 bg-sand-50 dark:bg-sand-950 px-3 py-2 text-sm text-sand-900 dark:text-sand-50 placeholder:text-sand-400 focus:border-[var(--brand-accent)] focus:outline-none"
304
+ aria-invalid={invalid}
305
+ aria-describedby={error ? "su-error" : undefined}
306
+ className={fieldClass}
273
307
  placeholder="you@example.com"
274
308
  />
275
309
  </div>
276
310
  <div>
277
- <label className="block text-xs font-medium text-sand-600 dark:text-sand-400 mb-1" htmlFor="su-password">
311
+ <label className="mb-1 block text-base font-medium text-sand-700 dark:text-sand-200" htmlFor="su-password">
278
312
  Password
279
313
  </label>
280
314
  <input
@@ -285,19 +319,18 @@ export function CheckoutSuccessClient({
285
319
  minLength={8}
286
320
  value={password}
287
321
  onChange={(e) => setPassword(e.target.value)}
288
- className="w-full rounded-lg border border-sand-200 dark:border-sand-800 bg-sand-50 dark:bg-sand-950 px-3 py-2 text-sm text-sand-900 dark:text-sand-50 placeholder:text-sand-400 focus:border-[var(--brand-accent)] focus:outline-none"
322
+ aria-invalid={invalid}
323
+ aria-describedby={error ? "su-error" : undefined}
324
+ className={fieldClass}
289
325
  placeholder="Min. 8 characters"
290
326
  />
291
327
  </div>
292
- {error && (
293
- <p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
294
- {error}
295
- </p>
296
- )}
328
+ <FieldError id="su-error" message={error} />
297
329
  <button
298
330
  type="submit"
299
331
  disabled={loading || !signUpLoaded}
300
- className="w-full rounded-lg bg-[var(--brand-accent)] py-2 text-sm font-semibold text-white transition-opacity hover:opacity-90 disabled:opacity-50"
332
+ className={filledButtonClass}
333
+ style={filledButtonStyle}
301
334
  >
302
335
  {loading ? "Creating account…" : "Create account & activate eSIM"}
303
336
  </button>
@@ -305,18 +338,17 @@ export function CheckoutSuccessClient({
305
338
  </>
306
339
  )}
307
340
 
308
- {/* Sign-in: credentials step */}
309
341
  {step === "credentials" && mode === "signin" && (
310
342
  <>
311
- <h2 className="text-base font-semibold text-sand-900 dark:text-sand-50 mb-1">Sign in</h2>
312
- <p className="text-xs text-sand-500 mb-5">
343
+ <h2 className="mb-1 text-base font-semibold text-sand-900 dark:text-sand-50">Sign in</h2>
344
+ <p className="mb-5 text-base text-sand-700 dark:text-sand-200">
313
345
  {emailLocked
314
346
  ? "Sign in with the email from your checkout to activate your eSIM."
315
347
  : "Sign in to activate your eSIM."}
316
348
  </p>
317
349
  <form onSubmit={handleSignIn} className="space-y-4">
318
350
  <div>
319
- <label className="block text-xs font-medium text-sand-600 dark:text-sand-400 mb-1" htmlFor="si-email">
351
+ <label className="mb-1 block text-base font-medium text-sand-700 dark:text-sand-200" htmlFor="si-email">
320
352
  Email address
321
353
  </label>
322
354
  <input
@@ -327,12 +359,14 @@ export function CheckoutSuccessClient({
327
359
  readOnly={emailLocked}
328
360
  value={email}
329
361
  onChange={(e) => setEmail(e.target.value)}
330
- className="w-full rounded-lg border border-sand-200 dark:border-sand-800 bg-sand-50 dark:bg-sand-950 px-3 py-2 text-sm text-sand-900 dark:text-sand-50 placeholder:text-sand-400 focus:border-[var(--brand-accent)] focus:outline-none"
362
+ aria-invalid={invalid}
363
+ aria-describedby={error ? "si-error" : undefined}
364
+ className={fieldClass}
331
365
  placeholder="you@example.com"
332
366
  />
333
367
  </div>
334
368
  <div>
335
- <label className="block text-xs font-medium text-sand-600 dark:text-sand-400 mb-1" htmlFor="si-password">
369
+ <label className="mb-1 block text-base font-medium text-sand-700 dark:text-sand-200" htmlFor="si-password">
336
370
  Password
337
371
  </label>
338
372
  <input
@@ -342,19 +376,18 @@ export function CheckoutSuccessClient({
342
376
  required
343
377
  value={password}
344
378
  onChange={(e) => setPassword(e.target.value)}
345
- className="w-full rounded-lg border border-sand-200 dark:border-sand-800 bg-sand-50 dark:bg-sand-950 px-3 py-2 text-sm text-sand-900 dark:text-sand-50 placeholder:text-sand-400 focus:border-[var(--brand-accent)] focus:outline-none"
379
+ aria-invalid={invalid}
380
+ aria-describedby={error ? "si-error" : undefined}
381
+ className={fieldClass}
346
382
  placeholder="Your password"
347
383
  />
348
384
  </div>
349
- {error && (
350
- <p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
351
- {error}
352
- </p>
353
- )}
385
+ <FieldError id="si-error" message={error} />
354
386
  <button
355
387
  type="submit"
356
388
  disabled={loading || !signInLoaded}
357
- className="w-full rounded-lg bg-[var(--brand-accent)] py-2 text-sm font-semibold text-white transition-opacity hover:opacity-90 disabled:opacity-50"
389
+ className={filledButtonClass}
390
+ style={filledButtonStyle}
358
391
  >
359
392
  {loading ? "Signing in…" : "Sign in & activate eSIM"}
360
393
  </button>
@@ -362,16 +395,15 @@ export function CheckoutSuccessClient({
362
395
  </>
363
396
  )}
364
397
 
365
- {/* Email verification step */}
366
398
  {step === "verify" && (
367
399
  <>
368
- <h2 className="text-base font-semibold text-sand-900 dark:text-sand-50 mb-1">Check your email</h2>
369
- <p className="text-xs text-sand-500 mb-5">
370
- We sent a 6-digit code to <span className="text-sand-600 dark:text-sand-300">{email}</span>.
400
+ <h2 className="mb-1 text-base font-semibold text-sand-900 dark:text-sand-50">Check your email</h2>
401
+ <p className="mb-5 text-base text-sand-700 dark:text-sand-200">
402
+ We sent a 6-digit code to <span className="text-[var(--brand-accent-text)]">{email}</span>.
371
403
  </p>
372
404
  <form onSubmit={handleVerify} className="space-y-4">
373
405
  <div>
374
- <label className="block text-xs font-medium text-sand-600 dark:text-sand-400 mb-1" htmlFor="verify-code">
406
+ <label className="mb-1 block text-base font-medium text-sand-700 dark:text-sand-200" htmlFor="verify-code">
375
407
  Verification code
376
408
  </label>
377
409
  <input
@@ -383,19 +415,18 @@ export function CheckoutSuccessClient({
383
415
  maxLength={6}
384
416
  value={code}
385
417
  onChange={(e) => setCode(e.target.value.replace(/\D/g, ""))}
386
- className="w-full rounded-lg border border-sand-200 dark:border-sand-800 bg-sand-50 dark:bg-sand-950 px-3 py-2 text-center text-lg tracking-[0.4em] text-sand-900 dark:text-sand-50 placeholder:text-sand-400 focus:border-[var(--brand-accent)] focus:outline-none"
418
+ aria-invalid={invalid}
419
+ aria-describedby={error ? "verify-error" : undefined}
420
+ className={`${fieldClass} text-center tracking-[0.4em]`}
387
421
  placeholder="000000"
388
422
  />
389
423
  </div>
390
- {error && (
391
- <p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
392
- {error}
393
- </p>
394
- )}
424
+ <FieldError id="verify-error" message={error} />
395
425
  <button
396
426
  type="submit"
397
427
  disabled={loading || code.length < 6}
398
- className="w-full rounded-lg bg-[var(--brand-accent)] py-2 text-sm font-semibold text-white transition-opacity hover:opacity-90 disabled:opacity-50"
428
+ className={filledButtonClass}
429
+ style={filledButtonStyle}
399
430
  >
400
431
  {loading ? "Verifying…" : "Verify & activate eSIM"}
401
432
  </button>
@@ -403,17 +434,20 @@ export function CheckoutSuccessClient({
403
434
  <button
404
435
  type="button"
405
436
  onClick={() => { setStep("credentials"); setError(null); setCode(""); }}
406
- className="mt-4 w-full text-center text-xs text-sand-400 hover:text-sand-500"
437
+ className="mt-4 inline-flex min-h-11 w-full items-center justify-center text-base text-[var(--brand-accent-text)]"
407
438
  >
408
- ← Back
439
+ Back
409
440
  </button>
410
441
  </>
411
442
  )}
412
443
  </div>
444
+ ) : null}
413
445
 
414
- <p className="text-center text-xs text-sand-400">
415
- Your payment is confirmed. Account setup is required to receive your eSIM QR code.
416
- </p>
446
+ {showAccount ? (
447
+ <p className="text-center text-base text-sand-700 dark:text-sand-200">
448
+ Create an account here to install the eSIM. This step does not charge you again.
449
+ </p>
450
+ ) : null}
417
451
  </div>
418
452
  </div>
419
453
  );
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, type ReactNode } from "react";
4
+
5
+ /** The only heading on a confirmation outcome. Focused once, when it mounts. */
6
+ export function FocusHeading({ children }: { children: ReactNode }) {
7
+ const heading = useRef<HTMLHeadingElement>(null);
8
+ useEffect(() => {
9
+ heading.current?.focus();
10
+ }, []);
11
+ return (
12
+ <h1
13
+ ref={heading}
14
+ tabIndex={-1}
15
+ className="text-2xl font-semibold text-sand-900 dark:text-sand-50"
16
+ >
17
+ {children}
18
+ </h1>
19
+ );
20
+ }