@carrierllc/mcp 0.2.15 → 0.2.17

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 (79) hide show
  1. package/dist/cli.js +644 -0
  2. package/dist/cli.js.map +1 -0
  3. package/dist/index.js +1264 -243
  4. package/dist/index.js.map +1 -1
  5. package/package.json +21 -14
  6. package/plugin/.claude-plugin/marketplace.json +31 -0
  7. package/plugin/carrier/.claude-plugin/plugin.json +19 -0
  8. package/plugin/carrier/.mcp.json +8 -0
  9. package/plugin/carrier/README.md +48 -0
  10. package/plugin/carrier/agents/carrier-billing-auditor.md +16 -0
  11. package/plugin/carrier/agents/carrier-fleet-ops.md +15 -0
  12. package/plugin/carrier/agents/carrier-storefront-builder.md +17 -0
  13. package/plugin/carrier/commands/billing.md +16 -0
  14. package/plugin/carrier/commands/churn.md +15 -0
  15. package/plugin/carrier/commands/credits.md +15 -0
  16. package/plugin/carrier/commands/esim-status.md +15 -0
  17. package/plugin/carrier/commands/fleet.md +17 -0
  18. package/plugin/carrier/commands/greenzone.md +16 -0
  19. package/plugin/carrier/commands/onboard.md +17 -0
  20. package/plugin/carrier/commands/packages.md +18 -0
  21. package/plugin/carrier/commands/provision.md +20 -0
  22. package/plugin/carrier/commands/sms.md +18 -0
  23. package/plugin/carrier/commands/status.md +15 -0
  24. package/plugin/carrier/commands/storefront.md +21 -0
  25. package/plugin/carrier/commands/subscribers.md +18 -0
  26. package/plugin/carrier/commands/usage.md +16 -0
  27. package/plugin/carrier/skills/carrier-operations/SKILL.md +36 -0
  28. package/templates/storefront/eslint.config.mjs +15 -0
  29. package/templates/storefront/next.config.ts +22 -0
  30. package/templates/storefront/open-next.config.ts +3 -0
  31. package/templates/storefront/package.json +38 -0
  32. package/templates/storefront/pnpm-lock.yaml +4020 -0
  33. package/templates/storefront/postcss.config.mjs +7 -0
  34. package/templates/storefront/src/app/activate/[orderId]/page.tsx +28 -0
  35. package/templates/storefront/src/app/apple-icon.tsx +29 -0
  36. package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +52 -0
  37. package/templates/storefront/src/app/checkout/[templateId]/page.tsx +24 -0
  38. package/templates/storefront/src/app/contact/page.tsx +35 -0
  39. package/templates/storefront/src/app/dashboard/page.tsx +26 -0
  40. package/templates/storefront/src/app/globals.css +99 -0
  41. package/templates/storefront/src/app/help/page.tsx +25 -0
  42. package/templates/storefront/src/app/icon.tsx +29 -0
  43. package/templates/storefront/src/app/layout.tsx +56 -0
  44. package/templates/storefront/src/app/legal/acceptable-use/page.tsx +21 -0
  45. package/templates/storefront/src/app/legal/privacy/page.tsx +27 -0
  46. package/templates/storefront/src/app/legal/terms/page.tsx +25 -0
  47. package/templates/storefront/src/app/manifest.ts +18 -0
  48. package/templates/storefront/src/app/page.tsx +31 -0
  49. package/templates/storefront/src/app/robots.ts +9 -0
  50. package/templates/storefront/src/app/shop/ShopClient.tsx +27 -0
  51. package/templates/storefront/src/app/shop/page.tsx +9 -0
  52. package/templates/storefront/src/app/sign-in/[[...sign-in]]/page.tsx +24 -0
  53. package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +13 -0
  54. package/templates/storefront/src/app/sitemap.ts +14 -0
  55. package/templates/storefront/src/brand.config.ts +28 -0
  56. package/templates/storefront/src/components/Providers.tsx +16 -0
  57. package/templates/storefront/src/components/faq/FaqSection.tsx +80 -0
  58. package/templates/storefront/src/components/footer/StorefrontFooter.tsx +61 -0
  59. package/templates/storefront/src/components/landing/HeroSection.tsx +52 -0
  60. package/templates/storefront/src/components/landing/PlanCard.tsx +61 -0
  61. package/templates/storefront/src/components/nav/StorefrontNav.tsx +69 -0
  62. package/templates/storefront/src/components/theme/clerk-appearance.ts +51 -0
  63. package/templates/storefront/src/components/theme/theme-provider.tsx +87 -0
  64. package/templates/storefront/src/components/theme/theme-script.tsx +24 -0
  65. package/templates/storefront/src/lib/conversion-events.ts +36 -0
  66. package/templates/storefront/src/lib/sanitize-auth-redirect.ts +16 -0
  67. package/templates/storefront/src/middleware.ts +25 -0
  68. package/templates/storefront/src/vendor/carrier/client.ts +95 -0
  69. package/templates/storefront/src/vendor/carrier/index.ts +2 -0
  70. package/templates/storefront/src/vendor/carrier/types.ts +21 -0
  71. package/templates/storefront/src/vendor/config/index.ts +2 -0
  72. package/templates/storefront/src/vendor/geo/index.ts +32 -0
  73. package/templates/storefront/src/vendor/ui/brand.ts +68 -0
  74. package/templates/storefront/src/vendor/ui/cn.ts +7 -0
  75. package/templates/storefront/src/vendor/ui/countryImagery.ts +46 -0
  76. package/templates/storefront/src/vendor/ui/index.ts +3 -0
  77. package/templates/storefront/tsconfig.json +23 -0
  78. package/templates/storefront/wrangler.jsonc +11 -0
  79. package/dist/.metadata_never_index +0 -0
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,28 @@
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: "Activate eSIM" };
7
+
8
+ interface Props {
9
+ params: Promise<{ orderId: string }>;
10
+ }
11
+
12
+ export default async function ActivatePage({ params }: Props) {
13
+ const { userId } = await auth();
14
+ if (!userId) redirect("/sign-in");
15
+ const { orderId } = await params;
16
+
17
+ return (
18
+ <div className="mx-auto max-w-lg px-4 py-24 text-center">
19
+ <h1 className="text-2xl font-bold text-white mb-4">Activate your eSIM</h1>
20
+ <p className="text-white/50 mb-8">Order ID: {orderId}</p>
21
+ <div className="rounded-2xl border border-white/10 bg-white/5 p-8">
22
+ <p className="text-white/60 text-sm">
23
+ Your QR code will appear here once your order is confirmed.
24
+ </p>
25
+ </div>
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,29 @@
1
+ import { ImageResponse } from "next/og";
2
+ import brand from "@/brand.config";
3
+
4
+ export const size = { width: 180, height: 180 };
5
+ export const contentType = "image/png";
6
+
7
+ export default function AppleIcon() {
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: "40px",
19
+ fontSize: "100px",
20
+ fontWeight: 700,
21
+ color: brand.colors.accent,
22
+ }}
23
+ >
24
+ {brand.name[0]}
25
+ </div>
26
+ ),
27
+ { ...size },
28
+ );
29
+ }
@@ -0,0 +1,52 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { useAuth } from "@clerk/nextjs";
5
+ import { useRouter } from "next/navigation";
6
+ import { conversionEvents } from "@/lib/conversion-events";
7
+ import type { SkuTemplate } from "@/vendor/carrier/types";
8
+
9
+ interface Props {
10
+ plan: SkuTemplate;
11
+ session: { url: string } | null;
12
+ }
13
+
14
+ export function CheckoutClient({ plan, session }: Props) {
15
+ const { isSignedIn, isLoaded } = useAuth();
16
+ const router = useRouter();
17
+ const [checkoutError, setCheckoutError] = useState<string | null>(null);
18
+
19
+ useEffect(() => {
20
+ if (!isLoaded) return;
21
+ if (!isSignedIn) {
22
+ sessionStorage.setItem("checkout_intent", plan.id);
23
+ router.push(`/sign-in?redirect_url=/checkout/${plan.id}`);
24
+ return;
25
+ }
26
+ conversionEvents.beginCheckout(plan.name, plan.price_cents / 100);
27
+ if (session?.url) {
28
+ window.location.href = session.url;
29
+ return;
30
+ }
31
+ setCheckoutError(
32
+ "Checkout is unavailable. Configure NEXT_PUBLIC_CARRIER_API_URL and Clerk keys, then try again.",
33
+ );
34
+ }, [isLoaded, isSignedIn, plan, session, router]);
35
+
36
+ if (checkoutError) {
37
+ return (
38
+ <div className="mx-auto max-w-lg px-4 py-32 text-center">
39
+ <p className="text-white/80">{checkoutError}</p>
40
+ <p className="text-sm text-white/30 mt-2">{plan.name}</p>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ return (
46
+ <div className="mx-auto max-w-lg px-4 py-32 text-center">
47
+ <div className="h-8 w-8 rounded-full border-2 border-[var(--brand-accent)] border-t-transparent animate-spin mx-auto mb-4" />
48
+ <p className="text-white/60">Preparing your checkout…</p>
49
+ <p className="text-sm text-white/30 mt-2">{plan.name}</p>
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,24 @@
1
+ export const dynamic = "force-dynamic";
2
+
3
+ import { notFound } from "next/navigation";
4
+ import { auth } from "@clerk/nextjs/server";
5
+ import { fetchTemplates, createCheckoutSession } from "@/vendor/carrier/client";
6
+ import { CheckoutClient } from "./CheckoutClient";
7
+
8
+ export const metadata = { title: "Checkout" };
9
+
10
+ interface Props {
11
+ params: Promise<{ templateId: string }>;
12
+ }
13
+
14
+ export default async function CheckoutPage({ params }: Props) {
15
+ const { templateId } = await params;
16
+ const { userId } = await auth();
17
+ const plans = await fetchTemplates();
18
+ const plan = plans.find((p) => p.id === templateId);
19
+ if (!plan) notFound();
20
+
21
+ const session = userId ? await createCheckoutSession(templateId, userId) : null;
22
+
23
+ return <CheckoutClient plan={plan} session={session} />;
24
+ }
@@ -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
+ }
@@ -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,13 @@
1
+ export const dynamic = "force-dynamic";
2
+
3
+ import { SignUp } from "@clerk/nextjs";
4
+
5
+ export const metadata = { title: "Sign up" };
6
+
7
+ export default function SignUpPage() {
8
+ return (
9
+ <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4">
10
+ <SignUp signInUrl="/sign-in" redirectUrl="/dashboard" />
11
+ </div>
12
+ );
13
+ }
@@ -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
+ }