@carrierllc/mcp 0.13.41 → 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.
- package/dist/{chunk-6OAZJBLA.js → chunk-4XUAUQ3U.js} +67 -7
- package/dist/chunk-4XUAUQ3U.js.map +1 -0
- package/dist/cli.js +55 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.js +66 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/storefront/CLERK.md +23 -0
- package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +29 -22
- package/templates/storefront/src/app/activate/[orderId]/page.tsx +2 -2
- package/templates/storefront/src/app/api/checkout/claim/route.ts +7 -6
- package/templates/storefront/src/app/api/checkout/guest/route.ts +72 -17
- package/templates/storefront/src/app/api/orders/[orderId]/route.ts +2 -2
- package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +51 -25
- package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +165 -131
- package/templates/storefront/src/app/checkout/success/FocusHeading.tsx +20 -0
- package/templates/storefront/src/app/checkout/success/page.tsx +136 -15
- package/templates/storefront/src/app/dashboard/EsimControls.tsx +20 -24
- package/templates/storefront/src/app/dashboard/page.tsx +43 -23
- package/templates/storefront/src/app/globals.css +44 -5
- package/templates/storefront/src/app/help/page.tsx +2 -2
- package/templates/storefront/src/app/layout.tsx +5 -3
- package/templates/storefront/src/app/page.tsx +27 -7
- package/templates/storefront/src/app/shop/ShopClient.tsx +106 -72
- package/templates/storefront/src/app/shop/page.tsx +7 -2
- package/templates/storefront/src/app/sign-in/[[...sign-in]]/StorefrontSignInClient.tsx +297 -0
- package/templates/storefront/src/app/sign-in/[[...sign-in]]/page.tsx +13 -14
- package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +31 -28
- package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +5 -2
- package/templates/storefront/src/brand.config.ts +16 -0
- package/templates/storefront/src/components/Providers.tsx +20 -1
- package/templates/storefront/src/components/faq/FaqSection.tsx +2 -2
- package/templates/storefront/src/components/footer/StorefrontFooter.tsx +7 -7
- package/templates/storefront/src/components/landing/FinalCTA.tsx +4 -9
- package/templates/storefront/src/components/landing/HeroSection.tsx +4 -7
- package/templates/storefront/src/components/landing/HowItWorks.tsx +1 -1
- package/templates/storefront/src/components/landing/PlanCard.tsx +23 -13
- package/templates/storefront/src/components/landing/TierPlanBoard.tsx +237 -0
- package/templates/storefront/src/components/nav/StorefrontNav.tsx +8 -11
- package/templates/storefront/src/components/shop/CountrySearch.tsx +94 -0
- package/templates/storefront/src/components/theme/BrandStyles.tsx +2 -2
- package/templates/storefront/src/components/theme/ThemeToggle.tsx +1 -1
- package/templates/storefront/src/components/theme/brand-accent-css.ts +21 -0
- package/templates/storefront/src/components/theme/brand-style.ts +88 -0
- package/templates/storefront/src/components/theme/clerk-appearance.ts +18 -2
- package/templates/storefront/src/lib/buyer-auth-copy.ts +83 -0
- package/templates/storefront/src/lib/buyer-error.ts +32 -0
- package/templates/storefront/src/lib/buyer-session.ts +41 -0
- package/templates/storefront/src/lib/catalog-countries.ts +179 -0
- package/templates/storefront/src/lib/checkout-client-error.ts +28 -0
- package/templates/storefront/src/lib/checkout-copy.ts +89 -0
- package/templates/storefront/src/lib/checkout-order-claim.ts +8 -7
- package/templates/storefront/src/lib/checkout-return.ts +18 -0
- package/templates/storefront/src/lib/clerk-instance.ts +89 -0
- package/templates/storefront/src/lib/order-reference.ts +15 -0
- package/templates/storefront/src/lib/request-origin.ts +28 -0
- package/templates/storefront/src/lib/sanitize-auth-redirect.ts +20 -4
- package/templates/storefront/src/lib/tap-target.ts +17 -0
- package/templates/storefront/src/lib/verify-checkout-session.ts +118 -19
- package/templates/storefront/src/middleware.ts +11 -0
- package/templates/storefront/src/vendor/carrier/catalog-guard.ts +9 -5
- package/templates/storefront/src/vendor/carrier/client.ts +22 -4
- package/templates/storefront/src/vendor/carrier/country-offers.ts +106 -0
- package/templates/storefront/src/vendor/carrier/stripe-price.ts +134 -0
- package/templates/storefront/src/vendor/carrier/tiers.ts +161 -0
- package/templates/storefront/src/vendor/carrier/types.ts +22 -0
- package/templates/storefront/src/vendor/geo/index.ts +15 -4
- package/dist/chunk-6OAZJBLA.js.map +0 -1
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* CheckoutSuccessClient
|
|
4
4
|
*
|
|
5
|
-
* Shown after Stripe
|
|
5
|
+
* Shown after a paid Stripe Checkout session for this shop.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
214
|
+
setError(accountStepError(err));
|
|
162
215
|
} finally {
|
|
163
216
|
setLoading(false);
|
|
164
217
|
}
|
|
165
218
|
}
|
|
166
219
|
|
|
167
|
-
|
|
220
|
+
const invalid = error ? true : undefined;
|
|
221
|
+
const showAccount = step === "credentials" || step === "verify";
|
|
168
222
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
<div className="
|
|
172
|
-
<
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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=
|
|
246
|
+
className={filledButtonClass}
|
|
247
|
+
style={filledButtonStyle}
|
|
200
248
|
>
|
|
201
249
|
Try again
|
|
202
250
|
</button>
|
|
203
251
|
</div>
|
|
204
|
-
|
|
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
|
-
{
|
|
221
|
-
<div className="rounded-2xl border border-sand-200 dark:border-sand-800
|
|
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
|
|
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
|
|
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
|
|
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
|
|
254
|
-
<p className="text-
|
|
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
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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=
|
|
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
|
|
312
|
-
<p className="text-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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=
|
|
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
|
|
369
|
-
<p className="text-
|
|
370
|
-
We sent a 6-digit code to <span className="text-
|
|
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-
|
|
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
|
-
|
|
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=
|
|
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
|
|
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
|
-
|
|
439
|
+
Back
|
|
409
440
|
</button>
|
|
410
441
|
</>
|
|
411
442
|
)}
|
|
412
443
|
</div>
|
|
444
|
+
) : null}
|
|
413
445
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
+
}
|