@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,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,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
|
+
}
|