@carrierllc/mcp 0.2.16 → 0.2.18
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 +21 -1
- package/dist/cli.js +1445 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +1437 -252
- package/dist/index.js.map +1 -1
- package/package.json +23 -15
- package/plugin/.claude-plugin/marketplace.json +31 -0
- package/plugin/carrier/.claude-plugin/plugin.json +19 -0
- package/plugin/carrier/.mcp.json +8 -0
- package/plugin/carrier/README.md +75 -0
- package/plugin/carrier/agents/carrier-billing-auditor.md +16 -0
- package/plugin/carrier/agents/carrier-fleet-ops.md +15 -0
- package/plugin/carrier/agents/carrier-storefront-builder.md +17 -0
- package/plugin/carrier/commands/billing.md +23 -0
- package/plugin/carrier/commands/churn.md +15 -0
- package/plugin/carrier/commands/credits.md +15 -0
- package/plugin/carrier/commands/esim-status.md +15 -0
- package/plugin/carrier/commands/fleet.md +17 -0
- package/plugin/carrier/commands/greenzone.md +16 -0
- package/plugin/carrier/commands/onboard.md +17 -0
- package/plugin/carrier/commands/packages.md +18 -0
- package/plugin/carrier/commands/provision.md +30 -0
- package/plugin/carrier/commands/sms.md +18 -0
- package/plugin/carrier/commands/status.md +15 -0
- package/plugin/carrier/commands/storefront.md +21 -0
- package/plugin/carrier/commands/subscribers.md +18 -0
- package/plugin/carrier/commands/usage.md +16 -0
- package/plugin/carrier/commands/wallet.md +31 -0
- package/plugin/carrier/skills/carrier-operations/SKILL.md +43 -0
- package/templates/storefront/eslint.config.mjs +15 -0
- package/templates/storefront/next.config.ts +22 -0
- package/templates/storefront/open-next.config.ts +3 -0
- package/templates/storefront/package.json +38 -0
- package/templates/storefront/pnpm-lock.yaml +4020 -0
- package/templates/storefront/postcss.config.mjs +7 -0
- package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +125 -0
- package/templates/storefront/src/app/activate/[orderId]/page.tsx +32 -0
- package/templates/storefront/src/app/api/checkout/claim/route.ts +30 -0
- package/templates/storefront/src/app/api/checkout/guest/route.ts +91 -0
- package/templates/storefront/src/app/api/profile/phone/route.ts +40 -0
- package/templates/storefront/src/app/apple-icon.tsx +29 -0
- package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +110 -0
- package/templates/storefront/src/app/checkout/[templateId]/page.tsx +24 -0
- package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +415 -0
- package/templates/storefront/src/app/checkout/success/page.tsx +59 -0
- package/templates/storefront/src/app/contact/page.tsx +35 -0
- package/templates/storefront/src/app/dashboard/page.tsx +26 -0
- package/templates/storefront/src/app/globals.css +99 -0
- package/templates/storefront/src/app/help/page.tsx +25 -0
- package/templates/storefront/src/app/icon.tsx +29 -0
- package/templates/storefront/src/app/layout.tsx +56 -0
- package/templates/storefront/src/app/legal/acceptable-use/page.tsx +21 -0
- package/templates/storefront/src/app/legal/privacy/page.tsx +27 -0
- package/templates/storefront/src/app/legal/terms/page.tsx +25 -0
- package/templates/storefront/src/app/manifest.ts +18 -0
- package/templates/storefront/src/app/page.tsx +31 -0
- package/templates/storefront/src/app/robots.ts +9 -0
- package/templates/storefront/src/app/shop/ShopClient.tsx +27 -0
- package/templates/storefront/src/app/shop/page.tsx +9 -0
- package/templates/storefront/src/app/sign-in/[[...sign-in]]/page.tsx +24 -0
- package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +199 -0
- package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +28 -0
- package/templates/storefront/src/app/sitemap.ts +14 -0
- package/templates/storefront/src/brand.config.ts +28 -0
- package/templates/storefront/src/components/Providers.tsx +16 -0
- package/templates/storefront/src/components/faq/FaqSection.tsx +80 -0
- package/templates/storefront/src/components/footer/StorefrontFooter.tsx +61 -0
- package/templates/storefront/src/components/landing/HeroSection.tsx +52 -0
- package/templates/storefront/src/components/landing/PlanCard.tsx +61 -0
- package/templates/storefront/src/components/nav/StorefrontNav.tsx +69 -0
- package/templates/storefront/src/components/theme/clerk-appearance.ts +51 -0
- package/templates/storefront/src/components/theme/theme-provider.tsx +87 -0
- package/templates/storefront/src/components/theme/theme-script.tsx +24 -0
- package/templates/storefront/src/lib/checkout-order-claim.ts +127 -0
- package/templates/storefront/src/lib/complete-email-sign-up.ts +50 -0
- package/templates/storefront/src/lib/conversion-events.ts +36 -0
- package/templates/storefront/src/lib/sanitize-auth-redirect.ts +16 -0
- package/templates/storefront/src/lib/verify-checkout-session.ts +53 -0
- package/templates/storefront/src/middleware.ts +31 -0
- package/templates/storefront/src/vendor/carrier/client.ts +95 -0
- package/templates/storefront/src/vendor/carrier/index.ts +2 -0
- package/templates/storefront/src/vendor/carrier/types.ts +21 -0
- package/templates/storefront/src/vendor/config/index.ts +2 -0
- package/templates/storefront/src/vendor/geo/index.ts +32 -0
- package/templates/storefront/src/vendor/ui/brand.ts +68 -0
- package/templates/storefront/src/vendor/ui/cn.ts +7 -0
- package/templates/storefront/src/vendor/ui/countryImagery.ts +46 -0
- package/templates/storefront/src/vendor/ui/index.ts +3 -0
- package/templates/storefront/tsconfig.json +23 -0
- package/templates/storefront/wrangler.jsonc +11 -0
- package/dist/.metadata_never_index +0 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* CheckoutSuccessClient
|
|
4
|
+
*
|
|
5
|
+
* MANGO-8/9: Shown after Stripe payment success.
|
|
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].
|
|
10
|
+
*
|
|
11
|
+
* The orderId is verified server-side before this component renders.
|
|
12
|
+
*/
|
|
13
|
+
import { useState, FormEvent, useEffect, useCallback } from "react";
|
|
14
|
+
import { useSignUp, useSignIn } from "@clerk/nextjs/legacy";
|
|
15
|
+
import { useClerk, useAuth } from "@clerk/nextjs";
|
|
16
|
+
import { useRouter } from "next/navigation";
|
|
17
|
+
import { completeEmailSignUpAfterVerify } from "@/lib/complete-email-sign-up";
|
|
18
|
+
|
|
19
|
+
type AuthMode = "signup" | "signin";
|
|
20
|
+
type FlowStep = "idle" | "credentials" | "verify" | "redirecting" | "claim-error";
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
orderId: string;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
templateId: string;
|
|
26
|
+
purchaserEmail: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function CheckoutSuccessClient({
|
|
30
|
+
orderId,
|
|
31
|
+
sessionId,
|
|
32
|
+
templateId,
|
|
33
|
+
purchaserEmail,
|
|
34
|
+
}: Props) {
|
|
35
|
+
const { isLoaded: authLoaded, isSignedIn } = useAuth();
|
|
36
|
+
const { signUp, isLoaded: signUpLoaded } = useSignUp();
|
|
37
|
+
const { signIn, isLoaded: signInLoaded } = useSignIn();
|
|
38
|
+
const { setActive } = useClerk();
|
|
39
|
+
const router = useRouter();
|
|
40
|
+
|
|
41
|
+
const activateUrl = `/activate/${encodeURIComponent(orderId)}`;
|
|
42
|
+
const [mode, setMode] = useState<AuthMode>("signup");
|
|
43
|
+
const [step, setStep] = useState<FlowStep>("idle");
|
|
44
|
+
const [email, setEmail] = useState(purchaserEmail ?? "");
|
|
45
|
+
const [password, setPassword] = useState("");
|
|
46
|
+
const [code, setCode] = useState("");
|
|
47
|
+
const [error, setError] = useState<string | null>(null);
|
|
48
|
+
const [loading, setLoading] = useState(false);
|
|
49
|
+
const emailLocked = Boolean(purchaserEmail);
|
|
50
|
+
|
|
51
|
+
const claimOrderAndRedirect = useCallback(async (afterAuth = false) => {
|
|
52
|
+
setStep("redirecting");
|
|
53
|
+
setError(null);
|
|
54
|
+
try {
|
|
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");
|
|
63
|
+
}
|
|
64
|
+
router.replace(activateUrl);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
setStep(afterAuth || isSignedIn ? "claim-error" : "credentials");
|
|
67
|
+
setError(err instanceof Error ? err.message : "Failed to link your purchase to this account");
|
|
68
|
+
}
|
|
69
|
+
}, [activateUrl, isSignedIn, router, sessionId, templateId]);
|
|
70
|
+
|
|
71
|
+
// If already signed in, claim the paid order before activating.
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!authLoaded) return;
|
|
74
|
+
if (isSignedIn) {
|
|
75
|
+
void claimOrderAndRedirect(true);
|
|
76
|
+
} else {
|
|
77
|
+
setStep("credentials");
|
|
78
|
+
}
|
|
79
|
+
}, [authLoaded, isSignedIn, claimOrderAndRedirect]);
|
|
80
|
+
|
|
81
|
+
function clerkErrMsg(err: unknown): string {
|
|
82
|
+
const e = err as { errors?: Array<{ longMessage?: string; message?: string }> };
|
|
83
|
+
return (
|
|
84
|
+
e?.errors?.[0]?.longMessage ??
|
|
85
|
+
e?.errors?.[0]?.message ??
|
|
86
|
+
"Something went wrong. Please try again."
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Sign-up flow ──────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
async function handleSignUp(e: FormEvent<HTMLFormElement>) {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
if (!signUpLoaded || !signUp) return;
|
|
95
|
+
if (purchaserEmail && email.toLowerCase() !== purchaserEmail.toLowerCase()) {
|
|
96
|
+
setError("Use the same email address you entered at checkout.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
setError(null);
|
|
100
|
+
setLoading(true);
|
|
101
|
+
try {
|
|
102
|
+
await signUp.create({ emailAddress: email, password });
|
|
103
|
+
await signUp.prepareEmailAddressVerification({ strategy: "email_code" });
|
|
104
|
+
setStep("verify");
|
|
105
|
+
} catch (err) {
|
|
106
|
+
setError(clerkErrMsg(err));
|
|
107
|
+
} finally {
|
|
108
|
+
setLoading(false);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handleVerify(e: FormEvent<HTMLFormElement>) {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
if (!signUpLoaded || !signUp) return;
|
|
115
|
+
setError(null);
|
|
116
|
+
setLoading(true);
|
|
117
|
+
try {
|
|
118
|
+
const result = await signUp.attemptEmailAddressVerification({ code });
|
|
119
|
+
const completion = await completeEmailSignUpAfterVerify(signUp, result);
|
|
120
|
+
if (completion.status === "complete") {
|
|
121
|
+
await setActive({ session: completion.sessionId });
|
|
122
|
+
await claimOrderAndRedirect(true);
|
|
123
|
+
} else {
|
|
124
|
+
setError(completion.message);
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
setError(clerkErrMsg(err));
|
|
128
|
+
} finally {
|
|
129
|
+
setLoading(false);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Sign-in flow ──────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
async function handleSignIn(e: FormEvent<HTMLFormElement>) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
if (!signInLoaded || !signIn) return;
|
|
138
|
+
if (purchaserEmail && email.toLowerCase() !== purchaserEmail.toLowerCase()) {
|
|
139
|
+
setError("Use the same email address you entered at checkout.");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
setError(null);
|
|
143
|
+
setLoading(true);
|
|
144
|
+
try {
|
|
145
|
+
const result = await signIn.create({
|
|
146
|
+
identifier: email,
|
|
147
|
+
password,
|
|
148
|
+
});
|
|
149
|
+
if (result.status === "complete" && result.createdSessionId) {
|
|
150
|
+
await setActive({ session: result.createdSessionId });
|
|
151
|
+
await claimOrderAndRedirect(true);
|
|
152
|
+
} else {
|
|
153
|
+
setError("Sign-in incomplete. Please try again.");
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
setError(clerkErrMsg(err));
|
|
157
|
+
} finally {
|
|
158
|
+
setLoading(false);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
if (step === "idle" || step === "redirecting") {
|
|
165
|
+
return (
|
|
166
|
+
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4">
|
|
167
|
+
<div className="text-center space-y-3">
|
|
168
|
+
<div className="mx-auto size-8 animate-spin rounded-full border-2 border-[var(--brand-accent)] border-t-transparent" />
|
|
169
|
+
<p className="text-sm text-white/60">
|
|
170
|
+
{step === "redirecting" ? "Activating your eSIM…" : "Loading…"}
|
|
171
|
+
</p>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (step === "claim-error") {
|
|
178
|
+
return (
|
|
179
|
+
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4 py-16">
|
|
180
|
+
<div className="w-full max-w-md space-y-6 text-center">
|
|
181
|
+
<div className="rounded-2xl border border-emerald-500/30 bg-emerald-500/10 px-6 py-4">
|
|
182
|
+
<p className="text-lg font-semibold text-emerald-300">✓ Payment successful!</p>
|
|
183
|
+
</div>
|
|
184
|
+
<div className="rounded-2xl border border-white/10 bg-white/5 p-8 backdrop-blur space-y-4">
|
|
185
|
+
<p className="text-sm text-white/80">We could not link this purchase to your account.</p>
|
|
186
|
+
{error && (
|
|
187
|
+
<p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
|
|
188
|
+
{error}
|
|
189
|
+
</p>
|
|
190
|
+
)}
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={() => void claimOrderAndRedirect(true)}
|
|
194
|
+
className="w-full rounded-lg bg-[var(--brand-accent)] py-2 text-sm font-semibold text-white transition-opacity hover:opacity-90"
|
|
195
|
+
>
|
|
196
|
+
Try again
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4 py-16">
|
|
206
|
+
<div className="w-full max-w-md space-y-6">
|
|
207
|
+
{/* Payment success banner */}
|
|
208
|
+
<div className="rounded-2xl border border-emerald-500/30 bg-emerald-500/10 px-6 py-4 text-center">
|
|
209
|
+
<p className="text-lg font-semibold text-emerald-300">✓ Payment successful!</p>
|
|
210
|
+
<p className="mt-1 text-sm text-emerald-200/70">
|
|
211
|
+
Create an account to activate your eSIM.
|
|
212
|
+
</p>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Auth card */}
|
|
216
|
+
<div className="rounded-2xl border border-white/10 bg-white/5 p-8 backdrop-blur">
|
|
217
|
+
{/* Mode toggle */}
|
|
218
|
+
{step === "credentials" && (
|
|
219
|
+
<div className="flex rounded-lg border border-white/10 p-1 mb-6">
|
|
220
|
+
<button
|
|
221
|
+
type="button"
|
|
222
|
+
onClick={() => { setMode("signup"); setError(null); }}
|
|
223
|
+
className={`flex-1 rounded-md py-1.5 text-sm font-medium transition-colors ${
|
|
224
|
+
mode === "signup"
|
|
225
|
+
? "bg-[var(--brand-accent)] text-white"
|
|
226
|
+
: "text-white/40 hover:text-white/70"
|
|
227
|
+
}`}
|
|
228
|
+
>
|
|
229
|
+
Create account
|
|
230
|
+
</button>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
onClick={() => { setMode("signin"); setError(null); }}
|
|
234
|
+
className={`flex-1 rounded-md py-1.5 text-sm font-medium transition-colors ${
|
|
235
|
+
mode === "signin"
|
|
236
|
+
? "bg-[var(--brand-accent)] text-white"
|
|
237
|
+
: "text-white/40 hover:text-white/70"
|
|
238
|
+
}`}
|
|
239
|
+
>
|
|
240
|
+
Sign in
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{/* Sign-up: credentials step */}
|
|
246
|
+
{step === "credentials" && mode === "signup" && (
|
|
247
|
+
<>
|
|
248
|
+
<h2 className="text-base font-semibold text-white mb-1">Create your account</h2>
|
|
249
|
+
<p className="text-xs text-white/40 mb-5">
|
|
250
|
+
{emailLocked
|
|
251
|
+
? "Use the email from your checkout to create your account."
|
|
252
|
+
: "Email and password — no phone required."}
|
|
253
|
+
</p>
|
|
254
|
+
<form onSubmit={handleSignUp} className="space-y-4">
|
|
255
|
+
<div>
|
|
256
|
+
<label className="block text-xs font-medium text-white/60 mb-1" htmlFor="su-email">
|
|
257
|
+
Email address
|
|
258
|
+
</label>
|
|
259
|
+
<input
|
|
260
|
+
id="su-email"
|
|
261
|
+
type="email"
|
|
262
|
+
autoComplete="email"
|
|
263
|
+
required
|
|
264
|
+
readOnly={emailLocked}
|
|
265
|
+
value={email}
|
|
266
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
267
|
+
className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm text-white placeholder-white/20 focus:border-[var(--brand-accent)] focus:outline-none"
|
|
268
|
+
placeholder="you@example.com"
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
<div>
|
|
272
|
+
<label className="block text-xs font-medium text-white/60 mb-1" htmlFor="su-password">
|
|
273
|
+
Password
|
|
274
|
+
</label>
|
|
275
|
+
<input
|
|
276
|
+
id="su-password"
|
|
277
|
+
type="password"
|
|
278
|
+
autoComplete="new-password"
|
|
279
|
+
required
|
|
280
|
+
minLength={8}
|
|
281
|
+
value={password}
|
|
282
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
283
|
+
className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm text-white placeholder-white/20 focus:border-[var(--brand-accent)] focus:outline-none"
|
|
284
|
+
placeholder="Min. 8 characters"
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
{error && (
|
|
288
|
+
<p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
|
|
289
|
+
{error}
|
|
290
|
+
</p>
|
|
291
|
+
)}
|
|
292
|
+
<button
|
|
293
|
+
type="submit"
|
|
294
|
+
disabled={loading || !signUpLoaded}
|
|
295
|
+
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"
|
|
296
|
+
>
|
|
297
|
+
{loading ? "Creating account…" : "Create account & activate eSIM"}
|
|
298
|
+
</button>
|
|
299
|
+
</form>
|
|
300
|
+
</>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
{/* Sign-in: credentials step */}
|
|
304
|
+
{step === "credentials" && mode === "signin" && (
|
|
305
|
+
<>
|
|
306
|
+
<h2 className="text-base font-semibold text-white mb-1">Sign in</h2>
|
|
307
|
+
<p className="text-xs text-white/40 mb-5">
|
|
308
|
+
{emailLocked
|
|
309
|
+
? "Sign in with the email from your checkout to activate your eSIM."
|
|
310
|
+
: "Sign in to activate your eSIM."}
|
|
311
|
+
</p>
|
|
312
|
+
<form onSubmit={handleSignIn} className="space-y-4">
|
|
313
|
+
<div>
|
|
314
|
+
<label className="block text-xs font-medium text-white/60 mb-1" htmlFor="si-email">
|
|
315
|
+
Email address
|
|
316
|
+
</label>
|
|
317
|
+
<input
|
|
318
|
+
id="si-email"
|
|
319
|
+
type="email"
|
|
320
|
+
autoComplete="email"
|
|
321
|
+
required
|
|
322
|
+
readOnly={emailLocked}
|
|
323
|
+
value={email}
|
|
324
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
325
|
+
className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm text-white placeholder-white/20 focus:border-[var(--brand-accent)] focus:outline-none"
|
|
326
|
+
placeholder="you@example.com"
|
|
327
|
+
/>
|
|
328
|
+
</div>
|
|
329
|
+
<div>
|
|
330
|
+
<label className="block text-xs font-medium text-white/60 mb-1" htmlFor="si-password">
|
|
331
|
+
Password
|
|
332
|
+
</label>
|
|
333
|
+
<input
|
|
334
|
+
id="si-password"
|
|
335
|
+
type="password"
|
|
336
|
+
autoComplete="current-password"
|
|
337
|
+
required
|
|
338
|
+
value={password}
|
|
339
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
340
|
+
className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm text-white placeholder-white/20 focus:border-[var(--brand-accent)] focus:outline-none"
|
|
341
|
+
placeholder="Your password"
|
|
342
|
+
/>
|
|
343
|
+
</div>
|
|
344
|
+
{error && (
|
|
345
|
+
<p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
|
|
346
|
+
{error}
|
|
347
|
+
</p>
|
|
348
|
+
)}
|
|
349
|
+
<button
|
|
350
|
+
type="submit"
|
|
351
|
+
disabled={loading || !signInLoaded}
|
|
352
|
+
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"
|
|
353
|
+
>
|
|
354
|
+
{loading ? "Signing in…" : "Sign in & activate eSIM"}
|
|
355
|
+
</button>
|
|
356
|
+
</form>
|
|
357
|
+
</>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
{/* Email verification step */}
|
|
361
|
+
{step === "verify" && (
|
|
362
|
+
<>
|
|
363
|
+
<h2 className="text-base font-semibold text-white mb-1">Check your email</h2>
|
|
364
|
+
<p className="text-xs text-white/40 mb-5">
|
|
365
|
+
We sent a 6-digit code to <span className="text-white/70">{email}</span>.
|
|
366
|
+
</p>
|
|
367
|
+
<form onSubmit={handleVerify} className="space-y-4">
|
|
368
|
+
<div>
|
|
369
|
+
<label className="block text-xs font-medium text-white/60 mb-1" htmlFor="verify-code">
|
|
370
|
+
Verification code
|
|
371
|
+
</label>
|
|
372
|
+
<input
|
|
373
|
+
id="verify-code"
|
|
374
|
+
type="text"
|
|
375
|
+
inputMode="numeric"
|
|
376
|
+
autoComplete="one-time-code"
|
|
377
|
+
required
|
|
378
|
+
maxLength={6}
|
|
379
|
+
value={code}
|
|
380
|
+
onChange={(e) => setCode(e.target.value.replace(/\D/g, ""))}
|
|
381
|
+
className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-center text-lg tracking-[0.4em] text-white placeholder-white/20 focus:border-[var(--brand-accent)] focus:outline-none"
|
|
382
|
+
placeholder="000000"
|
|
383
|
+
/>
|
|
384
|
+
</div>
|
|
385
|
+
{error && (
|
|
386
|
+
<p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
|
|
387
|
+
{error}
|
|
388
|
+
</p>
|
|
389
|
+
)}
|
|
390
|
+
<button
|
|
391
|
+
type="submit"
|
|
392
|
+
disabled={loading || code.length < 6}
|
|
393
|
+
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"
|
|
394
|
+
>
|
|
395
|
+
{loading ? "Verifying…" : "Verify & activate eSIM"}
|
|
396
|
+
</button>
|
|
397
|
+
</form>
|
|
398
|
+
<button
|
|
399
|
+
type="button"
|
|
400
|
+
onClick={() => { setStep("credentials"); setError(null); setCode(""); }}
|
|
401
|
+
className="mt-4 w-full text-center text-xs text-white/30 hover:text-white/50"
|
|
402
|
+
>
|
|
403
|
+
← Back
|
|
404
|
+
</button>
|
|
405
|
+
</>
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
<p className="text-center text-xs text-white/30">
|
|
410
|
+
Your payment is confirmed. Account setup is required to receive your eSIM QR code.
|
|
411
|
+
</p>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const dynamic = "force-dynamic";
|
|
2
|
+
/**
|
|
3
|
+
* /checkout/success
|
|
4
|
+
*
|
|
5
|
+
* MANGO-8/9: Post-payment landing page.
|
|
6
|
+
*
|
|
7
|
+
* The user arrives here after a successful Stripe Checkout payment.
|
|
8
|
+
* Query params from Stripe: ?templateId=<id>&session_id=<stripe_session_id>
|
|
9
|
+
*
|
|
10
|
+
* Flow:
|
|
11
|
+
* 1. Show payment success confirmation.
|
|
12
|
+
* 2. If already signed in → redirect to /activate/[orderId] immediately.
|
|
13
|
+
* 3. If not signed in → show email + password sign-up form (no phone).
|
|
14
|
+
* 4. After sign-up/sign-in → redirect to /activate/[orderId].
|
|
15
|
+
*
|
|
16
|
+
* The orderId is derived from the Stripe session_id (or templateId as fallback)
|
|
17
|
+
* so the activate page can look up the order.
|
|
18
|
+
*/
|
|
19
|
+
import { redirect } from "next/navigation";
|
|
20
|
+
import { Suspense } from "react";
|
|
21
|
+
import { verifyCheckoutSession } from "@/lib/verify-checkout-session";
|
|
22
|
+
import { CheckoutSuccessClient } from "./CheckoutSuccessClient";
|
|
23
|
+
|
|
24
|
+
export const metadata = { title: "Payment successful" };
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
searchParams: Promise<{ session_id?: string; templateId?: string }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default async function CheckoutSuccessPage({ searchParams }: Props) {
|
|
31
|
+
const params = await searchParams;
|
|
32
|
+
const sessionId = params.session_id ?? "";
|
|
33
|
+
const templateId = params.templateId ?? "";
|
|
34
|
+
|
|
35
|
+
const verification = await verifyCheckoutSession(sessionId, templateId);
|
|
36
|
+
if (!verification.ok) {
|
|
37
|
+
redirect("/shop");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Suspense
|
|
42
|
+
fallback={
|
|
43
|
+
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4">
|
|
44
|
+
<div className="text-center space-y-3">
|
|
45
|
+
<div className="mx-auto size-8 animate-spin rounded-full border-2 border-[var(--brand-accent)] border-t-transparent" />
|
|
46
|
+
<p className="text-sm text-white/60">Loading…</p>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
}
|
|
50
|
+
>
|
|
51
|
+
<CheckoutSuccessClient
|
|
52
|
+
orderId={verification.orderId}
|
|
53
|
+
sessionId={sessionId}
|
|
54
|
+
templateId={templateId}
|
|
55
|
+
purchaserEmail={verification.purchaserEmail}
|
|
56
|
+
/>
|
|
57
|
+
</Suspense>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import brand from "@/brand.config";
|
|
2
|
+
|
|
3
|
+
export const metadata = { title: "Contact" };
|
|
4
|
+
|
|
5
|
+
export default function ContactPage() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="mx-auto max-w-2xl px-4 py-24">
|
|
8
|
+
<h1 className="text-3xl font-bold text-white mb-8">Get in touch</h1>
|
|
9
|
+
<div className="space-y-4 text-white/70">
|
|
10
|
+
<p>
|
|
11
|
+
Email:{" "}
|
|
12
|
+
<a
|
|
13
|
+
href={`mailto:${brand.supportEmail}`}
|
|
14
|
+
className="text-[var(--brand-accent)] hover:underline"
|
|
15
|
+
>
|
|
16
|
+
{brand.supportEmail}
|
|
17
|
+
</a>
|
|
18
|
+
</p>
|
|
19
|
+
{brand.supportWhatsapp && (
|
|
20
|
+
<p>
|
|
21
|
+
WhatsApp:{" "}
|
|
22
|
+
<a
|
|
23
|
+
href={`https://wa.me/${brand.supportWhatsapp.replace(/[^0-9]/g, "")}`}
|
|
24
|
+
target="_blank"
|
|
25
|
+
rel="noopener noreferrer"
|
|
26
|
+
className="text-[var(--brand-accent)] hover:underline"
|
|
27
|
+
>
|
|
28
|
+
{brand.supportWhatsapp}
|
|
29
|
+
</a>
|
|
30
|
+
</p>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const dynamic = "force-dynamic";
|
|
2
|
+
|
|
3
|
+
import { auth } from "@clerk/nextjs/server";
|
|
4
|
+
import { redirect } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
export const metadata = { title: "Dashboard" };
|
|
7
|
+
|
|
8
|
+
export default async function DashboardPage() {
|
|
9
|
+
const { userId } = await auth();
|
|
10
|
+
if (!userId) redirect("/sign-in?redirect_url=/dashboard");
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="mx-auto max-w-4xl px-4 py-16">
|
|
14
|
+
<h1 className="text-3xl font-bold text-white mb-8">Your eSIMs</h1>
|
|
15
|
+
<div className="rounded-2xl border border-white/10 bg-white/5 p-12 text-center">
|
|
16
|
+
<p className="text-white/50">No active eSIMs yet.</p>
|
|
17
|
+
<a
|
|
18
|
+
href="/shop"
|
|
19
|
+
className="mt-4 inline-block rounded-full bg-[var(--brand-accent)] px-6 py-2 text-sm font-semibold text-white hover:bg-[var(--brand-accent-dark)] transition-colors"
|
|
20
|
+
>
|
|
21
|
+
Browse plans
|
|
22
|
+
</a>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
|
4
|
+
|
|
5
|
+
@theme {
|
|
6
|
+
/* Brand palette — overridden per white-label via CSS vars */
|
|
7
|
+
--color-brand-bg: var(--brand-bg);
|
|
8
|
+
--color-brand-accent: var(--brand-accent);
|
|
9
|
+
--color-brand-accent-dark: var(--brand-accent-dark);
|
|
10
|
+
--color-brand-accent-light: var(--brand-accent-light);
|
|
11
|
+
--color-brand-text: var(--brand-text);
|
|
12
|
+
--color-brand-text-secondary: var(--brand-text-secondary);
|
|
13
|
+
|
|
14
|
+
/* Map mango-* → brand flame so ported component classNames keep working */
|
|
15
|
+
--color-mango-50: #fff4ef;
|
|
16
|
+
--color-mango-100: #ffe0d0;
|
|
17
|
+
--color-mango-200: #ffbfa0;
|
|
18
|
+
--color-mango-300: #ff9970;
|
|
19
|
+
--color-mango-400: #ff7d4d;
|
|
20
|
+
--color-mango-500: var(--brand-accent);
|
|
21
|
+
--color-mango-600: var(--brand-accent-dark);
|
|
22
|
+
--color-mango-700: #b33a17;
|
|
23
|
+
--color-mango-800: #8a2d12;
|
|
24
|
+
--color-mango-900: #5e1e0c;
|
|
25
|
+
--color-mango-950: #3a1107;
|
|
26
|
+
|
|
27
|
+
/* Neutral slate */
|
|
28
|
+
--color-slate-950: #080C16;
|
|
29
|
+
|
|
30
|
+
/* Fonts */
|
|
31
|
+
--font-sans: var(--font-geist-sans, system-ui, sans-serif);
|
|
32
|
+
--font-mono: var(--font-geist-mono, monospace);
|
|
33
|
+
|
|
34
|
+
/* Radii */
|
|
35
|
+
--radius-sm: 0.25rem;
|
|
36
|
+
--radius-md: 0.5rem;
|
|
37
|
+
--radius-lg: 0.75rem;
|
|
38
|
+
--radius-xl: 1rem;
|
|
39
|
+
--radius-2xl: 1.5rem;
|
|
40
|
+
--radius-full: 9999px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ── Brand CSS variables (dark-first default) ─────────────────────────────── */
|
|
44
|
+
:root {
|
|
45
|
+
--brand-bg: #080C16;
|
|
46
|
+
--brand-accent: #FF6B35;
|
|
47
|
+
--brand-accent-dark: #D9461C;
|
|
48
|
+
--brand-accent-light: #FFB088;
|
|
49
|
+
--brand-text: #F5F1EA;
|
|
50
|
+
--brand-text-secondary: #C9CCD6;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
[data-theme="light"] {
|
|
54
|
+
--brand-bg: #FAFAF8;
|
|
55
|
+
--brand-text: #0F1117;
|
|
56
|
+
--brand-text-secondary: #6B7280;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ── Base reset ───────────────────────────────────────────────────────────── */
|
|
60
|
+
*,
|
|
61
|
+
*::before,
|
|
62
|
+
*::after {
|
|
63
|
+
box-sizing: border-box;
|
|
64
|
+
margin: 0;
|
|
65
|
+
padding: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
html {
|
|
69
|
+
scroll-behavior: smooth;
|
|
70
|
+
-webkit-text-size-adjust: 100%;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
body {
|
|
74
|
+
background-color: var(--brand-bg);
|
|
75
|
+
color: var(--brand-text);
|
|
76
|
+
font-family: var(--font-sans);
|
|
77
|
+
line-height: 1.6;
|
|
78
|
+
-webkit-font-smoothing: antialiased;
|
|
79
|
+
-moz-osx-font-smoothing: grayscale;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ── Selection ───────────────────────────────────────────────────────────── */
|
|
83
|
+
::selection {
|
|
84
|
+
background-color: color-mix(in srgb, var(--brand-accent) 30%, transparent);
|
|
85
|
+
color: var(--brand-text);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ── Scrollbar ───────────────────────────────────────────────────────────── */
|
|
89
|
+
::-webkit-scrollbar {
|
|
90
|
+
width: 6px;
|
|
91
|
+
height: 6px;
|
|
92
|
+
}
|
|
93
|
+
::-webkit-scrollbar-track {
|
|
94
|
+
background: transparent;
|
|
95
|
+
}
|
|
96
|
+
::-webkit-scrollbar-thumb {
|
|
97
|
+
background: color-mix(in srgb, var(--brand-accent) 40%, transparent);
|
|
98
|
+
border-radius: 3px;
|
|
99
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const metadata = { title: "Help" };
|
|
2
|
+
|
|
3
|
+
const STEPS = [
|
|
4
|
+
{ title: "1. Choose your plan", body: "Browse plans by region or data size and select one that suits your trip." },
|
|
5
|
+
{ title: "2. Create an account", body: "Sign up for free to manage your eSIMs in one place." },
|
|
6
|
+
{ title: "3. Complete checkout", body: "Pay securely. You will receive a QR code by email within seconds." },
|
|
7
|
+
{ title: "4. Scan & connect", body: "Open Settings on your device, scan the QR code, and you are online." },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export default function HelpPage() {
|
|
11
|
+
return (
|
|
12
|
+
<div className="mx-auto max-w-3xl px-4 py-24">
|
|
13
|
+
<h1 className="text-3xl font-bold text-white mb-4">How it works</h1>
|
|
14
|
+
<p className="text-white/50 mb-12">Get connected in under 5 minutes.</p>
|
|
15
|
+
<div className="grid gap-6">
|
|
16
|
+
{STEPS.map((step) => (
|
|
17
|
+
<div key={step.title} className="rounded-2xl border border-white/10 bg-white/5 p-6">
|
|
18
|
+
<h2 className="font-semibold text-white mb-2">{step.title}</h2>
|
|
19
|
+
<p className="text-sm text-white/60">{step.body}</p>
|
|
20
|
+
</div>
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|