@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.
Files changed (91) hide show
  1. package/README.md +21 -1
  2. package/dist/cli.js +1445 -0
  3. package/dist/cli.js.map +1 -0
  4. package/dist/index.js +1437 -252
  5. package/dist/index.js.map +1 -1
  6. package/package.json +23 -15
  7. package/plugin/.claude-plugin/marketplace.json +31 -0
  8. package/plugin/carrier/.claude-plugin/plugin.json +19 -0
  9. package/plugin/carrier/.mcp.json +8 -0
  10. package/plugin/carrier/README.md +75 -0
  11. package/plugin/carrier/agents/carrier-billing-auditor.md +16 -0
  12. package/plugin/carrier/agents/carrier-fleet-ops.md +15 -0
  13. package/plugin/carrier/agents/carrier-storefront-builder.md +17 -0
  14. package/plugin/carrier/commands/billing.md +23 -0
  15. package/plugin/carrier/commands/churn.md +15 -0
  16. package/plugin/carrier/commands/credits.md +15 -0
  17. package/plugin/carrier/commands/esim-status.md +15 -0
  18. package/plugin/carrier/commands/fleet.md +17 -0
  19. package/plugin/carrier/commands/greenzone.md +16 -0
  20. package/plugin/carrier/commands/onboard.md +17 -0
  21. package/plugin/carrier/commands/packages.md +18 -0
  22. package/plugin/carrier/commands/provision.md +30 -0
  23. package/plugin/carrier/commands/sms.md +18 -0
  24. package/plugin/carrier/commands/status.md +15 -0
  25. package/plugin/carrier/commands/storefront.md +21 -0
  26. package/plugin/carrier/commands/subscribers.md +18 -0
  27. package/plugin/carrier/commands/usage.md +16 -0
  28. package/plugin/carrier/commands/wallet.md +31 -0
  29. package/plugin/carrier/skills/carrier-operations/SKILL.md +43 -0
  30. package/templates/storefront/eslint.config.mjs +15 -0
  31. package/templates/storefront/next.config.ts +22 -0
  32. package/templates/storefront/open-next.config.ts +3 -0
  33. package/templates/storefront/package.json +38 -0
  34. package/templates/storefront/pnpm-lock.yaml +4020 -0
  35. package/templates/storefront/postcss.config.mjs +7 -0
  36. package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +125 -0
  37. package/templates/storefront/src/app/activate/[orderId]/page.tsx +32 -0
  38. package/templates/storefront/src/app/api/checkout/claim/route.ts +30 -0
  39. package/templates/storefront/src/app/api/checkout/guest/route.ts +91 -0
  40. package/templates/storefront/src/app/api/profile/phone/route.ts +40 -0
  41. package/templates/storefront/src/app/apple-icon.tsx +29 -0
  42. package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +110 -0
  43. package/templates/storefront/src/app/checkout/[templateId]/page.tsx +24 -0
  44. package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +415 -0
  45. package/templates/storefront/src/app/checkout/success/page.tsx +59 -0
  46. package/templates/storefront/src/app/contact/page.tsx +35 -0
  47. package/templates/storefront/src/app/dashboard/page.tsx +26 -0
  48. package/templates/storefront/src/app/globals.css +99 -0
  49. package/templates/storefront/src/app/help/page.tsx +25 -0
  50. package/templates/storefront/src/app/icon.tsx +29 -0
  51. package/templates/storefront/src/app/layout.tsx +56 -0
  52. package/templates/storefront/src/app/legal/acceptable-use/page.tsx +21 -0
  53. package/templates/storefront/src/app/legal/privacy/page.tsx +27 -0
  54. package/templates/storefront/src/app/legal/terms/page.tsx +25 -0
  55. package/templates/storefront/src/app/manifest.ts +18 -0
  56. package/templates/storefront/src/app/page.tsx +31 -0
  57. package/templates/storefront/src/app/robots.ts +9 -0
  58. package/templates/storefront/src/app/shop/ShopClient.tsx +27 -0
  59. package/templates/storefront/src/app/shop/page.tsx +9 -0
  60. package/templates/storefront/src/app/sign-in/[[...sign-in]]/page.tsx +24 -0
  61. package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +199 -0
  62. package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +28 -0
  63. package/templates/storefront/src/app/sitemap.ts +14 -0
  64. package/templates/storefront/src/brand.config.ts +28 -0
  65. package/templates/storefront/src/components/Providers.tsx +16 -0
  66. package/templates/storefront/src/components/faq/FaqSection.tsx +80 -0
  67. package/templates/storefront/src/components/footer/StorefrontFooter.tsx +61 -0
  68. package/templates/storefront/src/components/landing/HeroSection.tsx +52 -0
  69. package/templates/storefront/src/components/landing/PlanCard.tsx +61 -0
  70. package/templates/storefront/src/components/nav/StorefrontNav.tsx +69 -0
  71. package/templates/storefront/src/components/theme/clerk-appearance.ts +51 -0
  72. package/templates/storefront/src/components/theme/theme-provider.tsx +87 -0
  73. package/templates/storefront/src/components/theme/theme-script.tsx +24 -0
  74. package/templates/storefront/src/lib/checkout-order-claim.ts +127 -0
  75. package/templates/storefront/src/lib/complete-email-sign-up.ts +50 -0
  76. package/templates/storefront/src/lib/conversion-events.ts +36 -0
  77. package/templates/storefront/src/lib/sanitize-auth-redirect.ts +16 -0
  78. package/templates/storefront/src/lib/verify-checkout-session.ts +53 -0
  79. package/templates/storefront/src/middleware.ts +31 -0
  80. package/templates/storefront/src/vendor/carrier/client.ts +95 -0
  81. package/templates/storefront/src/vendor/carrier/index.ts +2 -0
  82. package/templates/storefront/src/vendor/carrier/types.ts +21 -0
  83. package/templates/storefront/src/vendor/config/index.ts +2 -0
  84. package/templates/storefront/src/vendor/geo/index.ts +32 -0
  85. package/templates/storefront/src/vendor/ui/brand.ts +68 -0
  86. package/templates/storefront/src/vendor/ui/cn.ts +7 -0
  87. package/templates/storefront/src/vendor/ui/countryImagery.ts +46 -0
  88. package/templates/storefront/src/vendor/ui/index.ts +3 -0
  89. package/templates/storefront/tsconfig.json +23 -0
  90. package/templates/storefront/wrangler.jsonc +11 -0
  91. package/dist/.metadata_never_index +0 -0
@@ -0,0 +1,29 @@
1
+ import { ImageResponse } from "next/og";
2
+ import brand from "@/brand.config";
3
+
4
+ export const size = { width: 32, height: 32 };
5
+ export const contentType = "image/png";
6
+
7
+ export default function Icon() {
8
+ return new ImageResponse(
9
+ (
10
+ <div
11
+ style={{
12
+ width: "100%",
13
+ height: "100%",
14
+ display: "flex",
15
+ alignItems: "center",
16
+ justifyContent: "center",
17
+ background: brand.colors.bg,
18
+ borderRadius: "6px",
19
+ fontSize: "18px",
20
+ fontWeight: 700,
21
+ color: brand.colors.accent,
22
+ }}
23
+ >
24
+ {brand.name[0]}
25
+ </div>
26
+ ),
27
+ { ...size },
28
+ );
29
+ }
@@ -0,0 +1,56 @@
1
+ export const dynamic = "force-dynamic";
2
+
3
+ import type { Metadata, Viewport } from "next";
4
+ import { GeistSans } from "geist/font/sans";
5
+ import { GeistMono } from "geist/font/mono";
6
+ import { Providers } from "@/components/Providers";
7
+ import { ThemeScript } from "@/components/theme/theme-script";
8
+ import { StorefrontNav } from "@/components/nav/StorefrontNav";
9
+ import { StorefrontFooter } from "@/components/footer/StorefrontFooter";
10
+ import brand from "@/brand.config";
11
+ import "./globals.css";
12
+
13
+ export const metadata: Metadata = {
14
+ title: {
15
+ default: brand.name,
16
+ template: `%s — ${brand.name}`,
17
+ },
18
+ description: brand.tagline,
19
+ metadataBase: new URL(`https://${brand.domain}`),
20
+ openGraph: {
21
+ siteName: brand.name,
22
+ locale: "en_US",
23
+ type: "website",
24
+ },
25
+ twitter: {
26
+ card: "summary_large_image",
27
+ creator: brand.social.x,
28
+ },
29
+ };
30
+
31
+ export const viewport: Viewport = {
32
+ themeColor: brand.colors.accent,
33
+ colorScheme: "dark light",
34
+ };
35
+
36
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
37
+ return (
38
+ <html
39
+ lang="en"
40
+ data-theme="dark"
41
+ className={`${GeistSans.variable} ${GeistMono.variable}`}
42
+ suppressHydrationWarning
43
+ >
44
+ <head>
45
+ <ThemeScript />
46
+ </head>
47
+ <body>
48
+ <Providers>
49
+ <StorefrontNav />
50
+ <main className="pt-16 min-h-screen">{children}</main>
51
+ <StorefrontFooter />
52
+ </Providers>
53
+ </body>
54
+ </html>
55
+ );
56
+ }
@@ -0,0 +1,21 @@
1
+ import brand from "@/brand.config";
2
+
3
+ export const metadata = { title: "Acceptable Use Policy" };
4
+
5
+ export default function AcceptableUsePage() {
6
+ return (
7
+ <div className="mx-auto max-w-2xl px-4 py-24 prose prose-invert">
8
+ <h1>Acceptable Use Policy</h1>
9
+ <p className="lead">Last updated: {new Date().getFullYear()}</p>
10
+ <p>
11
+ You may not use {brand.name} services for illegal activity, spamming, or any activity that
12
+ degrades network quality for other users. Violation may result in immediate account
13
+ termination without refund.
14
+ </p>
15
+ <h2>Contact</h2>
16
+ <p>
17
+ <a href={`mailto:${brand.supportEmail}`}>{brand.supportEmail}</a>
18
+ </p>
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,27 @@
1
+ import brand from "@/brand.config";
2
+
3
+ export const metadata = { title: "Privacy Policy" };
4
+
5
+ export default function PrivacyPage() {
6
+ return (
7
+ <div className="mx-auto max-w-2xl px-4 py-24 prose prose-invert">
8
+ <h1>Privacy Policy</h1>
9
+ <p className="lead">Last updated: {new Date().getFullYear()}</p>
10
+ <p>
11
+ {brand.legalName} (&ldquo;{brand.name}&rdquo;, &ldquo;we&rdquo;, &ldquo;us&rdquo;) operates{" "}
12
+ {brand.domain}. This policy explains how we collect, use, and protect your information.
13
+ </p>
14
+ <h2>Data we collect</h2>
15
+ <p>
16
+ We collect the minimum data necessary to provide eSIM services: your email address for
17
+ account creation, payment information processed by our payment provider, and device
18
+ information required for eSIM provisioning.
19
+ </p>
20
+ <h2>Contact</h2>
21
+ <p>
22
+ Questions? Email{" "}
23
+ <a href={`mailto:${brand.supportEmail}`}>{brand.supportEmail}</a>.
24
+ </p>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,25 @@
1
+ import brand from "@/brand.config";
2
+
3
+ export const metadata = { title: "Terms of Service" };
4
+
5
+ export default function TermsPage() {
6
+ return (
7
+ <div className="mx-auto max-w-2xl px-4 py-24 prose prose-invert">
8
+ <h1>Terms of Service</h1>
9
+ <p className="lead">Last updated: {new Date().getFullYear()}</p>
10
+ <p>
11
+ By using {brand.name} you agree to these terms. {brand.legalName} reserves the right to
12
+ suspend accounts that violate our Acceptable Use Policy.
13
+ </p>
14
+ <h2>eSIM Services</h2>
15
+ <p>
16
+ eSIM plans are non-refundable once the QR code has been scanned and the SIM activated.
17
+ Data allowances are valid for the period stated on the plan.
18
+ </p>
19
+ <h2>Contact</h2>
20
+ <p>
21
+ <a href={`mailto:${brand.supportEmail}`}>{brand.supportEmail}</a>
22
+ </p>
23
+ </div>
24
+ );
25
+ }
@@ -0,0 +1,18 @@
1
+ import type { MetadataRoute } from "next";
2
+ import brand from "@/brand.config";
3
+
4
+ export default function manifest(): MetadataRoute.Manifest {
5
+ return {
6
+ name: brand.name,
7
+ short_name: brand.name,
8
+ description: brand.tagline,
9
+ start_url: "/",
10
+ display: "standalone",
11
+ background_color: brand.colors.bg,
12
+ theme_color: brand.colors.accent,
13
+ icons: [
14
+ { src: "/brand/icon-192.png", sizes: "192x192", type: "image/png" },
15
+ { src: "/brand/icon-512.png", sizes: "512x512", type: "image/png" },
16
+ ],
17
+ };
18
+ }
@@ -0,0 +1,31 @@
1
+ import { HeroSection } from "@/components/landing/HeroSection";
2
+ import { FaqSection } from "@/components/faq/FaqSection";
3
+ import { fetchTemplates } from "@/vendor/carrier/client";
4
+ import { PlanCard } from "@/components/landing/PlanCard";
5
+
6
+ export default async function HomePage() {
7
+ const plans = await fetchTemplates();
8
+ const featured = plans.slice(0, 3);
9
+
10
+ return (
11
+ <>
12
+ <HeroSection />
13
+
14
+ <section className="py-24 bg-[var(--brand-bg)]">
15
+ <div className="mx-auto max-w-6xl px-4">
16
+ <h2 className="text-3xl font-bold text-white text-center mb-4">Popular plans</h2>
17
+ <p className="text-center text-white/50 mb-12">
18
+ Instant activation. No contracts. Cancel anytime.
19
+ </p>
20
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
21
+ {featured.map((plan, i) => (
22
+ <PlanCard key={plan.id} plan={plan} featured={i === 1} />
23
+ ))}
24
+ </div>
25
+ </div>
26
+ </section>
27
+
28
+ <FaqSection />
29
+ </>
30
+ );
31
+ }
@@ -0,0 +1,9 @@
1
+ import type { MetadataRoute } from "next";
2
+ import brand from "@/brand.config";
3
+
4
+ export default function robots(): MetadataRoute.Robots {
5
+ return {
6
+ rules: { userAgent: "*", allow: "/", disallow: ["/dashboard", "/activate"] },
7
+ sitemap: `https://${brand.domain}/sitemap.xml`,
8
+ };
9
+ }
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import { PlanCard } from "@/components/landing/PlanCard";
5
+ import { conversionEvents } from "@/lib/conversion-events";
6
+ import type { SkuTemplate } from "@/vendor/carrier/types";
7
+
8
+ export function ShopClient({ plans }: { plans: SkuTemplate[] }) {
9
+ const router = useRouter();
10
+
11
+ function handleSelect(plan: SkuTemplate) {
12
+ conversionEvents.selectPlan(plan.name);
13
+ router.push(`/checkout/${plan.id}`);
14
+ }
15
+
16
+ return (
17
+ <div className="mx-auto max-w-6xl px-4 py-16">
18
+ <h1 className="text-4xl font-bold text-white mb-2">Choose your plan</h1>
19
+ <p className="text-white/50 mb-12">Instant activation. No contracts.</p>
20
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
21
+ {plans.map((plan, i) => (
22
+ <PlanCard key={plan.id} plan={plan} featured={i === 1} onSelect={handleSelect} />
23
+ ))}
24
+ </div>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,9 @@
1
+ import { fetchTemplates } from "@/vendor/carrier/client";
2
+ import { ShopClient } from "./ShopClient";
3
+
4
+ export const metadata = { title: "Shop" };
5
+
6
+ export default async function ShopPage() {
7
+ const plans = await fetchTemplates();
8
+ return <ShopClient plans={plans} />;
9
+ }
@@ -0,0 +1,24 @@
1
+ export const dynamic = "force-dynamic";
2
+
3
+ import { SignIn } from "@clerk/nextjs";
4
+ import { sanitizeAuthRedirect } from "@/lib/sanitize-auth-redirect";
5
+
6
+ interface Props {
7
+ searchParams: Promise<{ redirect_url?: string }>;
8
+ }
9
+
10
+ export const metadata = { title: "Sign in" };
11
+
12
+ export default async function SignInPage({ searchParams }: Props) {
13
+ const { redirect_url } = await searchParams;
14
+ const redirectUrl = sanitizeAuthRedirect(redirect_url, "/dashboard");
15
+
16
+ return (
17
+ <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4">
18
+ <SignIn
19
+ redirectUrl={redirectUrl}
20
+ signUpUrl="/sign-up"
21
+ />
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,199 @@
1
+ "use client";
2
+ /**
3
+ * StorefrontSignUpClient
4
+ *
5
+ * MANGO-6/7: Custom email + password sign-up for the storefront.
6
+ * Phone number is NEVER required. It is collected optionally on /activate/[orderId].
7
+ *
8
+ * Clerk v6 API (storefront uses @clerk/nextjs ^6):
9
+ * useSignUp() → { signUp, isLoaded }
10
+ * useClerk() → { setActive }
11
+ */
12
+ import { useState, FormEvent } from "react";
13
+ import { useSignUp } from "@clerk/nextjs/legacy";
14
+ import { useClerk } from "@clerk/nextjs";
15
+ import { useSearchParams, useRouter } from "next/navigation";
16
+ import { completeEmailSignUpAfterVerify } from "@/lib/complete-email-sign-up";
17
+ import { sanitizeAuthRedirect } from "@/lib/sanitize-auth-redirect";
18
+
19
+ type FlowStep = "credentials" | "verify" | "complete";
20
+
21
+ export function StorefrontSignUpClient() {
22
+ const searchParams = useSearchParams();
23
+ const rawRedirect = searchParams.get("redirect_url") ?? undefined;
24
+ const redirectTarget = sanitizeAuthRedirect(rawRedirect, "/dashboard");
25
+
26
+ const { signUp, isLoaded } = useSignUp();
27
+ const { setActive } = useClerk();
28
+ const router = useRouter();
29
+
30
+ const [step, setStep] = useState<FlowStep>("credentials");
31
+ const [email, setEmail] = useState("");
32
+ const [password, setPassword] = useState("");
33
+ const [code, setCode] = useState("");
34
+ const [error, setError] = useState<string | null>(null);
35
+ const [loading, setLoading] = useState(false);
36
+
37
+ function clerkErrMsg(err: unknown): string {
38
+ const e = err as { errors?: Array<{ longMessage?: string; message?: string }> };
39
+ return (
40
+ e?.errors?.[0]?.longMessage ??
41
+ e?.errors?.[0]?.message ??
42
+ "Something went wrong. Please try again."
43
+ );
44
+ }
45
+
46
+ async function handleCredentials(e: FormEvent<HTMLFormElement>) {
47
+ e.preventDefault();
48
+ if (!isLoaded || !signUp) return;
49
+ setError(null);
50
+ setLoading(true);
51
+ try {
52
+ await signUp.create({ emailAddress: email, password });
53
+ await signUp.prepareEmailAddressVerification({ strategy: "email_code" });
54
+ setStep("verify");
55
+ } catch (err) {
56
+ setError(clerkErrMsg(err));
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ }
61
+
62
+ async function handleVerify(e: FormEvent<HTMLFormElement>) {
63
+ e.preventDefault();
64
+ if (!isLoaded || !signUp) return;
65
+ setError(null);
66
+ setLoading(true);
67
+ try {
68
+ const result = await signUp.attemptEmailAddressVerification({ code });
69
+ const completion = await completeEmailSignUpAfterVerify(signUp, result);
70
+ if (completion.status === "complete") {
71
+ setStep("complete");
72
+ await setActive({ session: completion.sessionId });
73
+ router.replace(redirectTarget);
74
+ } else {
75
+ setError(completion.message);
76
+ }
77
+ } catch (err) {
78
+ setError(clerkErrMsg(err));
79
+ } finally {
80
+ setLoading(false);
81
+ }
82
+ }
83
+
84
+ return (
85
+ <div className="w-full max-w-sm rounded-2xl border border-white/10 bg-white/5 p-8 backdrop-blur">
86
+ {step === "credentials" && (
87
+ <>
88
+ <h1 className="text-xl font-bold text-white mb-1">Create account</h1>
89
+ <p className="text-xs text-white/40 mb-6">Email and password — no phone required.</p>
90
+ <form onSubmit={handleCredentials} className="space-y-4">
91
+ <div>
92
+ <label className="block text-xs font-medium text-white/60 mb-1" htmlFor="sf-email">
93
+ Email address
94
+ </label>
95
+ <input
96
+ id="sf-email"
97
+ type="email"
98
+ autoComplete="email"
99
+ required
100
+ value={email}
101
+ onChange={(e) => setEmail(e.target.value)}
102
+ 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"
103
+ placeholder="you@example.com"
104
+ />
105
+ </div>
106
+ <div>
107
+ <label className="block text-xs font-medium text-white/60 mb-1" htmlFor="sf-password">
108
+ Password
109
+ </label>
110
+ <input
111
+ id="sf-password"
112
+ type="password"
113
+ autoComplete="new-password"
114
+ required
115
+ minLength={8}
116
+ value={password}
117
+ onChange={(e) => setPassword(e.target.value)}
118
+ 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"
119
+ placeholder="Min. 8 characters"
120
+ />
121
+ </div>
122
+ {error && (
123
+ <p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
124
+ {error}
125
+ </p>
126
+ )}
127
+ <button
128
+ type="submit"
129
+ disabled={loading || !isLoaded}
130
+ 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"
131
+ >
132
+ {loading ? "Creating account…" : "Continue"}
133
+ </button>
134
+ </form>
135
+ <p className="mt-4 text-center text-xs text-white/30">
136
+ Already have an account?{" "}
137
+ <a href="/sign-in" className="text-[var(--brand-accent)] hover:underline">
138
+ Sign in
139
+ </a>
140
+ </p>
141
+ </>
142
+ )}
143
+
144
+ {step === "verify" && (
145
+ <>
146
+ <h1 className="text-xl font-bold text-white mb-1">Check your email</h1>
147
+ <p className="text-xs text-white/40 mb-6">
148
+ We sent a 6-digit code to <span className="text-white/70">{email}</span>.
149
+ </p>
150
+ <form onSubmit={handleVerify} className="space-y-4">
151
+ <div>
152
+ <label className="block text-xs font-medium text-white/60 mb-1" htmlFor="sf-code">
153
+ Verification code
154
+ </label>
155
+ <input
156
+ id="sf-code"
157
+ type="text"
158
+ inputMode="numeric"
159
+ autoComplete="one-time-code"
160
+ required
161
+ maxLength={6}
162
+ value={code}
163
+ onChange={(e) => setCode(e.target.value.replace(/\D/g, ""))}
164
+ 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"
165
+ placeholder="000000"
166
+ />
167
+ </div>
168
+ {error && (
169
+ <p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
170
+ {error}
171
+ </p>
172
+ )}
173
+ <button
174
+ type="submit"
175
+ disabled={loading || code.length < 6}
176
+ 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"
177
+ >
178
+ {loading ? "Verifying…" : "Verify email"}
179
+ </button>
180
+ </form>
181
+ <button
182
+ type="button"
183
+ onClick={() => { setStep("credentials"); setError(null); setCode(""); }}
184
+ className="mt-4 w-full text-center text-xs text-white/30 hover:text-white/50"
185
+ >
186
+ ← Back
187
+ </button>
188
+ </>
189
+ )}
190
+
191
+ {step === "complete" && (
192
+ <div className="text-center space-y-3 py-4">
193
+ <div className="mx-auto size-8 animate-spin rounded-full border-2 border-[var(--brand-accent)] border-t-transparent" />
194
+ <p className="text-sm text-white/60">Setting up your account…</p>
195
+ </div>
196
+ )}
197
+ </div>
198
+ );
199
+ }
@@ -0,0 +1,28 @@
1
+ export const dynamic = "force-dynamic";
2
+ /**
3
+ * /sign-up — Custom email + password sign-up flow for the storefront.
4
+ *
5
+ * MANGO-6/7: Phone number is never required here. It is collected optionally
6
+ * on /activate/[orderId] after purchase.
7
+ */
8
+ import { Suspense } from "react";
9
+ import { StorefrontSignUpClient } from "./StorefrontSignUpClient";
10
+
11
+ export const metadata = { title: "Sign up" };
12
+
13
+ export default function SignUpPage() {
14
+ return (
15
+ <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4">
16
+ <Suspense
17
+ fallback={
18
+ <div className="text-center space-y-3">
19
+ <div className="mx-auto size-8 animate-spin rounded-full border-2 border-[var(--brand-accent)] border-t-transparent" />
20
+ <p className="text-sm text-white/60">Loading…</p>
21
+ </div>
22
+ }
23
+ >
24
+ <StorefrontSignUpClient />
25
+ </Suspense>
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,14 @@
1
+ import type { MetadataRoute } from "next";
2
+ import brand from "@/brand.config";
3
+
4
+ export default function sitemap(): MetadataRoute.Sitemap {
5
+ const base = `https://${brand.domain}`;
6
+ return [
7
+ { url: base, lastModified: new Date(), changeFrequency: "weekly", priority: 1 },
8
+ { url: `${base}/shop`, lastModified: new Date(), changeFrequency: "daily", priority: 0.9 },
9
+ { url: `${base}/help`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.5 },
10
+ { url: `${base}/contact`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.4 },
11
+ { url: `${base}/legal/privacy`, lastModified: new Date(), changeFrequency: "yearly", priority: 0.2 },
12
+ { url: `${base}/legal/terms`, lastModified: new Date(), changeFrequency: "yearly", priority: 0.2 },
13
+ ];
14
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * White-label brand configuration.
3
+ * Edit this file to re-skin the storefront for any brand.
4
+ */
5
+ const brand = {
6
+ name: process.env.NEXT_PUBLIC_BRAND_NAME ?? "Carrier",
7
+ legalName: "Lifecycle Innovations Limited",
8
+ tagline: "Programmable connectivity, on demand.",
9
+ domain: "carrier.llc",
10
+ supportUrl: "https://carrier.llc/help",
11
+ supportEmail: "support@carrier.llc",
12
+ supportWhatsapp: "+17864604829",
13
+ colors: {
14
+ bg: "#080C16",
15
+ accent: "#FF6B35",
16
+ accentDark: "#D9461C",
17
+ accentLight: "#FFB088",
18
+ text: "#F5F1EA",
19
+ textSecondary: "#C9CCD6",
20
+ },
21
+ social: {
22
+ x: "@carrier_llc",
23
+ instagram: "@carrier.llc",
24
+ tiktok: "@carrier.llc",
25
+ },
26
+ } as const;
27
+
28
+ export default brand;
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ import { ClerkProvider } from "@clerk/nextjs";
4
+ import type { ComponentProps } from "react";
5
+ import { ThemeProvider } from "@/components/theme/theme-provider";
6
+ import { clerkAppearance } from "@/components/theme/clerk-appearance";
7
+
8
+ type ClerkAppearance = ComponentProps<typeof ClerkProvider>["appearance"];
9
+
10
+ export function Providers({ children }: { children: React.ReactNode }) {
11
+ return (
12
+ <ClerkProvider appearance={clerkAppearance as ClerkAppearance}>
13
+ <ThemeProvider>{children}</ThemeProvider>
14
+ </ClerkProvider>
15
+ );
16
+ }
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+ import { cn } from "@/vendor/ui/cn";
6
+
7
+ const FAQS = [
8
+ {
9
+ q: "What is an eSIM?",
10
+ a: "An eSIM (embedded SIM) is a digital SIM that lets you activate a mobile plan without a physical SIM card. You scan a QR code and your device is connected instantly.",
11
+ },
12
+ {
13
+ q: "Which devices are compatible?",
14
+ a: "Most modern smartphones, tablets, and smartwatches support eSIM — including iPhone XS and later, Samsung Galaxy S21+, Google Pixel 3a+, and many more.",
15
+ },
16
+ {
17
+ q: "How quickly does the eSIM activate?",
18
+ a: "Activation is immediate after purchase. You will receive a QR code by email within seconds. Scan it in your device settings and you are connected.",
19
+ },
20
+ {
21
+ q: "What happens when my data runs out?",
22
+ a: "You will receive a notification when you are running low. You can purchase a top-up or a new plan from the dashboard at any time.",
23
+ },
24
+ {
25
+ q: "Can I use my eSIM in multiple countries?",
26
+ a: "Yes — regional plans cover multiple countries. Check the plan details for the exact coverage area before purchasing.",
27
+ },
28
+ ];
29
+
30
+ function FaqItem({ q, a }: { q: string; a: string }) {
31
+ const [open, setOpen] = useState(false);
32
+ return (
33
+ <div className="border-b border-white/10">
34
+ <button
35
+ className="flex w-full items-center justify-between py-4 text-left text-white hover:text-[var(--brand-accent)] transition-colors"
36
+ onClick={() => setOpen((o) => !o)}
37
+ >
38
+ <span className="font-medium">{q}</span>
39
+ <span
40
+ className={cn(
41
+ "ml-4 shrink-0 text-[var(--brand-accent)] transition-transform",
42
+ open && "rotate-45",
43
+ )}
44
+ >
45
+ +
46
+ </span>
47
+ </button>
48
+ <AnimatePresence>
49
+ {open && (
50
+ <motion.div
51
+ initial={{ height: 0, opacity: 0 }}
52
+ animate={{ height: "auto", opacity: 1 }}
53
+ exit={{ height: 0, opacity: 0 }}
54
+ transition={{ duration: 0.2 }}
55
+ className="overflow-hidden"
56
+ >
57
+ <p className="pb-4 text-sm text-white/60">{a}</p>
58
+ </motion.div>
59
+ )}
60
+ </AnimatePresence>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ export function FaqSection() {
66
+ return (
67
+ <section className="py-24 bg-[var(--brand-bg)]">
68
+ <div className="mx-auto max-w-2xl px-4">
69
+ <h2 className="text-3xl font-bold text-white text-center mb-12">
70
+ Frequently asked questions
71
+ </h2>
72
+ <div>
73
+ {FAQS.map((faq) => (
74
+ <FaqItem key={faq.q} q={faq.q} a={faq.a} />
75
+ ))}
76
+ </div>
77
+ </div>
78
+ </section>
79
+ );
80
+ }