@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.
- package/dist/cli.js +644 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +1264 -243
- package/dist/index.js.map +1 -1
- package/package.json +21 -14
- 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 +48 -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 +16 -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 +20 -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/skills/carrier-operations/SKILL.md +36 -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]/page.tsx +28 -0
- package/templates/storefront/src/app/apple-icon.tsx +29 -0
- package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +52 -0
- package/templates/storefront/src/app/checkout/[templateId]/page.tsx +24 -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]]/page.tsx +13 -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/conversion-events.ts +36 -0
- package/templates/storefront/src/lib/sanitize-auth-redirect.ts +16 -0
- package/templates/storefront/src/middleware.ts +25 -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,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} (“{brand.name}”, “we”, “us”) 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
|
+
}
|