@carrierllc/mcp 0.13.41 → 0.13.47

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 (68) hide show
  1. package/dist/{chunk-6OAZJBLA.js → chunk-4XUAUQ3U.js} +67 -7
  2. package/dist/chunk-4XUAUQ3U.js.map +1 -0
  3. package/dist/cli.js +55 -9
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +66 -15
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/templates/storefront/CLERK.md +23 -0
  9. package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +29 -22
  10. package/templates/storefront/src/app/activate/[orderId]/page.tsx +2 -2
  11. package/templates/storefront/src/app/api/checkout/claim/route.ts +7 -6
  12. package/templates/storefront/src/app/api/checkout/guest/route.ts +72 -17
  13. package/templates/storefront/src/app/api/orders/[orderId]/route.ts +2 -2
  14. package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +51 -25
  15. package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +165 -131
  16. package/templates/storefront/src/app/checkout/success/FocusHeading.tsx +20 -0
  17. package/templates/storefront/src/app/checkout/success/page.tsx +136 -15
  18. package/templates/storefront/src/app/dashboard/EsimControls.tsx +20 -24
  19. package/templates/storefront/src/app/dashboard/page.tsx +43 -23
  20. package/templates/storefront/src/app/globals.css +44 -5
  21. package/templates/storefront/src/app/help/page.tsx +2 -2
  22. package/templates/storefront/src/app/layout.tsx +5 -3
  23. package/templates/storefront/src/app/page.tsx +27 -7
  24. package/templates/storefront/src/app/shop/ShopClient.tsx +106 -72
  25. package/templates/storefront/src/app/shop/page.tsx +7 -2
  26. package/templates/storefront/src/app/sign-in/[[...sign-in]]/StorefrontSignInClient.tsx +297 -0
  27. package/templates/storefront/src/app/sign-in/[[...sign-in]]/page.tsx +13 -14
  28. package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +31 -28
  29. package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +5 -2
  30. package/templates/storefront/src/brand.config.ts +16 -0
  31. package/templates/storefront/src/components/Providers.tsx +20 -1
  32. package/templates/storefront/src/components/faq/FaqSection.tsx +2 -2
  33. package/templates/storefront/src/components/footer/StorefrontFooter.tsx +7 -7
  34. package/templates/storefront/src/components/landing/FinalCTA.tsx +4 -9
  35. package/templates/storefront/src/components/landing/HeroSection.tsx +4 -7
  36. package/templates/storefront/src/components/landing/HowItWorks.tsx +1 -1
  37. package/templates/storefront/src/components/landing/PlanCard.tsx +23 -13
  38. package/templates/storefront/src/components/landing/TierPlanBoard.tsx +237 -0
  39. package/templates/storefront/src/components/nav/StorefrontNav.tsx +8 -11
  40. package/templates/storefront/src/components/shop/CountrySearch.tsx +94 -0
  41. package/templates/storefront/src/components/theme/BrandStyles.tsx +2 -2
  42. package/templates/storefront/src/components/theme/ThemeToggle.tsx +1 -1
  43. package/templates/storefront/src/components/theme/brand-accent-css.ts +21 -0
  44. package/templates/storefront/src/components/theme/brand-style.ts +88 -0
  45. package/templates/storefront/src/components/theme/clerk-appearance.ts +18 -2
  46. package/templates/storefront/src/lib/buyer-auth-copy.ts +83 -0
  47. package/templates/storefront/src/lib/buyer-error.ts +32 -0
  48. package/templates/storefront/src/lib/buyer-session.ts +41 -0
  49. package/templates/storefront/src/lib/catalog-countries.ts +179 -0
  50. package/templates/storefront/src/lib/checkout-client-error.ts +28 -0
  51. package/templates/storefront/src/lib/checkout-copy.ts +89 -0
  52. package/templates/storefront/src/lib/checkout-order-claim.ts +8 -7
  53. package/templates/storefront/src/lib/checkout-return.ts +18 -0
  54. package/templates/storefront/src/lib/clerk-instance.ts +89 -0
  55. package/templates/storefront/src/lib/order-reference.ts +15 -0
  56. package/templates/storefront/src/lib/request-origin.ts +28 -0
  57. package/templates/storefront/src/lib/sanitize-auth-redirect.ts +20 -4
  58. package/templates/storefront/src/lib/tap-target.ts +17 -0
  59. package/templates/storefront/src/lib/verify-checkout-session.ts +118 -19
  60. package/templates/storefront/src/middleware.ts +11 -0
  61. package/templates/storefront/src/vendor/carrier/catalog-guard.ts +9 -5
  62. package/templates/storefront/src/vendor/carrier/client.ts +22 -4
  63. package/templates/storefront/src/vendor/carrier/country-offers.ts +106 -0
  64. package/templates/storefront/src/vendor/carrier/stripe-price.ts +134 -0
  65. package/templates/storefront/src/vendor/carrier/tiers.ts +161 -0
  66. package/templates/storefront/src/vendor/carrier/types.ts +22 -0
  67. package/templates/storefront/src/vendor/geo/index.ts +15 -4
  68. package/dist/chunk-6OAZJBLA.js.map +0 -1
@@ -15,14 +15,18 @@ import { useSignUp } from "@clerk/nextjs";
15
15
  import { useClerk } from "@clerk/nextjs";
16
16
  import { useSearchParams, useRouter } from "next/navigation";
17
17
  import { completeEmailSignUpAfterVerify } from "@/lib/complete-email-sign-up";
18
+ import { buyerCopyForClerkError } from "@/lib/buyer-auth-copy";
18
19
  import { sanitizeAuthRedirect } from "@/lib/sanitize-auth-redirect";
20
+ import { fieldClass, primaryButtonClass, textLinkClass } from "@/lib/tap-target";
19
21
 
20
22
  type FlowStep = "credentials" | "verify" | "complete";
21
23
 
22
- export function StorefrontSignUpClient() {
24
+ const SIGN_UP_ERROR_ID = "su-error";
25
+
26
+ export function StorefrontSignUpClient({ origin }: { origin: string }) {
23
27
  const searchParams = useSearchParams();
24
28
  const rawRedirect = searchParams.get("redirect_url") ?? undefined;
25
- const redirectTarget = sanitizeAuthRedirect(rawRedirect, "/dashboard");
29
+ const redirectTarget = sanitizeAuthRedirect(rawRedirect, "/dashboard", origin || undefined);
26
30
 
27
31
  const { signUp, isLoaded } = useSignUp();
28
32
  const { setActive } = useClerk();
@@ -34,15 +38,8 @@ export function StorefrontSignUpClient() {
34
38
  const [code, setCode] = useState("");
35
39
  const [error, setError] = useState<string | null>(null);
36
40
  const [loading, setLoading] = useState(false);
37
-
38
- function clerkErrMsg(err: unknown): string {
39
- const e = err as { errors?: Array<{ longMessage?: string; message?: string }> };
40
- return (
41
- e?.errors?.[0]?.longMessage ??
42
- e?.errors?.[0]?.message ??
43
- "Something went wrong. Please try again."
44
- );
45
- }
41
+ const describedBy = error ? SIGN_UP_ERROR_ID : undefined;
42
+ const invalid = error ? true : undefined;
46
43
 
47
44
  async function handleCredentials(e: FormEvent<HTMLFormElement>) {
48
45
  e.preventDefault();
@@ -54,7 +51,7 @@ export function StorefrontSignUpClient() {
54
51
  await signUp.prepareEmailAddressVerification({ strategy: "email_code" });
55
52
  setStep("verify");
56
53
  } catch (err) {
57
- setError(clerkErrMsg(err));
54
+ setError(buyerCopyForClerkError(err, "Something went wrong. Please try again."));
58
55
  } finally {
59
56
  setLoading(false);
60
57
  }
@@ -76,7 +73,7 @@ export function StorefrontSignUpClient() {
76
73
  setError(completion.message);
77
74
  }
78
75
  } catch (err) {
79
- setError(clerkErrMsg(err));
76
+ setError(buyerCopyForClerkError(err, "Something went wrong. Please try again."));
80
77
  } finally {
81
78
  setLoading(false);
82
79
  }
@@ -87,10 +84,10 @@ export function StorefrontSignUpClient() {
87
84
  {step === "credentials" && (
88
85
  <>
89
86
  <h1 className="text-xl font-bold text-sand-900 dark:text-sand-50 mb-1">Create account</h1>
90
- <p className="text-xs text-sand-500 mb-6">Email and password — no phone required.</p>
87
+ <p className="mb-6 text-base text-sand-500">Email and password — no phone required.</p>
91
88
  <form onSubmit={handleCredentials} className="space-y-4">
92
89
  <div>
93
- <label className="block text-xs font-medium text-sand-600 dark:text-sand-400 mb-1" htmlFor="sf-email">
90
+ <label className="mb-1 block text-base font-medium text-sand-600 dark:text-sand-400" htmlFor="sf-email">
94
91
  Email address
95
92
  </label>
96
93
  <input
@@ -100,12 +97,14 @@ export function StorefrontSignUpClient() {
100
97
  required
101
98
  value={email}
102
99
  onChange={(e) => setEmail(e.target.value)}
103
- className="w-full rounded-lg border border-sand-200 dark:border-sand-800 bg-sand-50 dark:bg-sand-950 px-3 py-2 text-sm text-sand-900 dark:text-sand-50 placeholder:text-sand-400 focus:border-[var(--brand-accent)] focus:outline-none"
100
+ className={fieldClass}
104
101
  placeholder="you@example.com"
102
+ aria-invalid={invalid}
103
+ aria-describedby={describedBy}
105
104
  />
106
105
  </div>
107
106
  <div>
108
- <label className="block text-xs font-medium text-sand-600 dark:text-sand-400 mb-1" htmlFor="sf-password">
107
+ <label className="mb-1 block text-base font-medium text-sand-600 dark:text-sand-400" htmlFor="sf-password">
109
108
  Password
110
109
  </label>
111
110
  <input
@@ -116,26 +115,28 @@ export function StorefrontSignUpClient() {
116
115
  minLength={8}
117
116
  value={password}
118
117
  onChange={(e) => setPassword(e.target.value)}
119
- className="w-full rounded-lg border border-sand-200 dark:border-sand-800 bg-sand-50 dark:bg-sand-950 px-3 py-2 text-sm text-sand-900 dark:text-sand-50 placeholder:text-sand-400 focus:border-[var(--brand-accent)] focus:outline-none"
118
+ className={fieldClass}
120
119
  placeholder="Min. 8 characters"
120
+ aria-invalid={invalid}
121
+ aria-describedby={describedBy}
121
122
  />
122
123
  </div>
123
124
  {error && (
124
- <p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
125
+ <p id={SIGN_UP_ERROR_ID} role="alert" className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-base text-red-700 dark:text-red-300">
125
126
  {error}
126
127
  </p>
127
128
  )}
128
129
  <button
129
130
  type="submit"
130
131
  disabled={loading || !isLoaded}
131
- 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"
132
+ className={primaryButtonClass}
132
133
  >
133
134
  {loading ? "Creating account…" : "Continue"}
134
135
  </button>
135
136
  </form>
136
- <p className="mt-4 text-center text-xs text-sand-400">
137
+ <p className="mt-4 text-center text-base text-sand-500">
137
138
  Already have an account?{" "}
138
- <Link href="/sign-in" className="text-[var(--brand-accent)] hover:underline">
139
+ <Link href={`/sign-in?redirect_url=${encodeURIComponent(redirectTarget)}`} className={textLinkClass}>
139
140
  Sign in
140
141
  </Link>
141
142
  </p>
@@ -145,12 +146,12 @@ export function StorefrontSignUpClient() {
145
146
  {step === "verify" && (
146
147
  <>
147
148
  <h1 className="text-xl font-bold text-sand-900 dark:text-sand-50 mb-1">Check your email</h1>
148
- <p className="text-xs text-sand-500 mb-6">
149
+ <p className="mb-6 text-base text-sand-500">
149
150
  We sent a 6-digit code to <span className="text-sand-600 dark:text-sand-300">{email}</span>.
150
151
  </p>
151
152
  <form onSubmit={handleVerify} className="space-y-4">
152
153
  <div>
153
- <label className="block text-xs font-medium text-sand-600 dark:text-sand-400 mb-1" htmlFor="sf-code">
154
+ <label className="mb-1 block text-base font-medium text-sand-600 dark:text-sand-400" htmlFor="sf-code">
154
155
  Verification code
155
156
  </label>
156
157
  <input
@@ -162,19 +163,21 @@ export function StorefrontSignUpClient() {
162
163
  maxLength={6}
163
164
  value={code}
164
165
  onChange={(e) => setCode(e.target.value.replace(/\D/g, ""))}
165
- className="w-full rounded-lg border border-sand-200 dark:border-sand-800 bg-sand-50 dark:bg-sand-950 px-3 py-2 text-center text-lg tracking-[0.4em] text-sand-900 dark:text-sand-50 placeholder:text-sand-400 focus:border-[var(--brand-accent)] focus:outline-none"
166
+ className={`${fieldClass} text-center tracking-[0.4em]`}
166
167
  placeholder="000000"
168
+ aria-invalid={invalid}
169
+ aria-describedby={describedBy}
167
170
  />
168
171
  </div>
169
172
  {error && (
170
- <p className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-300">
173
+ <p id={SIGN_UP_ERROR_ID} role="alert" className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-base text-red-700 dark:text-red-300">
171
174
  {error}
172
175
  </p>
173
176
  )}
174
177
  <button
175
178
  type="submit"
176
179
  disabled={loading || code.length < 6}
177
- 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"
180
+ className={primaryButtonClass}
178
181
  >
179
182
  {loading ? "Verifying…" : "Verify email"}
180
183
  </button>
@@ -182,7 +185,7 @@ export function StorefrontSignUpClient() {
182
185
  <button
183
186
  type="button"
184
187
  onClick={() => { setStep("credentials"); setError(null); setCode(""); }}
185
- className="mt-4 w-full text-center text-xs text-sand-400 hover:text-sand-500"
188
+ className={`${textLinkClass} mt-4 w-full justify-center`}
186
189
  >
187
190
  ← Back
188
191
  </button>
@@ -6,11 +6,14 @@ export const dynamic = "force-dynamic";
6
6
  * on /activate/[orderId] after purchase.
7
7
  */
8
8
  import { Suspense } from "react";
9
+ import { headers } from "next/headers";
10
+ import { requestOrigin } from "@/lib/request-origin";
9
11
  import { StorefrontSignUpClient } from "./StorefrontSignUpClient";
10
12
 
11
13
  export const metadata = { title: "Sign up" };
12
14
 
13
- export default function SignUpPage() {
15
+ export default async function SignUpPage() {
16
+ const origin = requestOrigin(await headers());
14
17
  return (
15
18
  <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center px-4">
16
19
  <Suspense
@@ -21,7 +24,7 @@ export default function SignUpPage() {
21
24
  </div>
22
25
  }
23
26
  >
24
- <StorefrontSignUpClient />
27
+ <StorefrontSignUpClient origin={origin} />
25
28
  </Suspense>
26
29
  </div>
27
30
  );
@@ -4,6 +4,11 @@
4
4
  */
5
5
  const brand = {
6
6
  name: process.env.NEXT_PUBLIC_BRAND_NAME ?? "Carrier",
7
+ /**
8
+ * Buyer password reset. Unset shows the Forgot password path.
9
+ * Set NEXT_PUBLIC_PASSWORD_RESET_ENABLED=false to hide it on this storefront.
10
+ */
11
+ passwordResetEnabled: process.env.NEXT_PUBLIC_PASSWORD_RESET_ENABLED !== "false",
7
12
  legalName: "Lifecycle Innovations Limited",
8
13
  tagline: "Programmable connectivity, on demand.",
9
14
  /** One sentence. Used for <meta name="description"> and social cards. */
@@ -32,11 +37,22 @@ const brand = {
32
37
  colors: {
33
38
  bg: "#FAFAF7",
34
39
  accent: "#FF6B35",
40
+ /** Text on a filled #FF6B35 button. */
41
+ onAccent: "#111111",
42
+ /** Accent text, links, and focus. Passes on the page background. */
43
+ accentText: "#C2410C",
44
+ /** Dark-mode accent text. Must stay ahead of the light value. */
45
+ accentTextDark: "#FF8A5C",
35
46
  accentDark: "#D9461C",
36
47
  accentLight: "#FFB088",
37
48
  text: "#1A1812",
38
49
  textSecondary: "#5C5644",
39
50
  },
51
+ /**
52
+ * Lightning tier. Off unless this tenant sets NEXT_PUBLIC_LIGHTNING_TIER=1
53
+ * and the plans carry real Stripe Price ids.
54
+ */
55
+ lightningTier: process.env.NEXT_PUBLIC_LIGHTNING_TIER === "1",
40
56
  social: {
41
57
  x: "@carrier_llc",
42
58
  instagram: "@carrier.llc",
@@ -2,14 +2,33 @@
2
2
 
3
3
  import { ClerkProvider } from "@clerk/nextjs";
4
4
  import type { ComponentProps } from "react";
5
+ import brand from "@/brand.config";
5
6
  import { ThemeProvider } from "@/components/theme/theme-provider";
6
7
  import { clerkAppearance } from "@/components/theme/clerk-appearance";
8
+ import { clerkPublishableKey } from "@/lib/clerk-instance";
7
9
 
8
10
  type ClerkAppearance = ComponentProps<typeof ClerkProvider>["appearance"];
9
11
 
10
12
  export function Providers({ children }: { children: React.ReactNode }) {
13
+ const publishableKey = clerkPublishableKey();
11
14
  return (
12
- <ClerkProvider appearance={clerkAppearance as ClerkAppearance}>
15
+ <ClerkProvider
16
+ {...(publishableKey ? { publishableKey } : {})}
17
+ appearance={clerkAppearance as ClerkAppearance}
18
+ localization={{
19
+ signIn: {
20
+ start: {
21
+ title: `Sign in to ${brand.name}`,
22
+ subtitle: "Email and password.",
23
+ },
24
+ },
25
+ }}
26
+ // Buyers never pick an organization. Clerk's choose-organization task
27
+ // lands on the dashboard, which accepts that pending session.
28
+ signInUrl="/sign-in"
29
+ signUpUrl="/sign-up"
30
+ taskUrls={{ "choose-organization": "/dashboard" }}
31
+ >
13
32
  <ThemeProvider>{children}</ThemeProvider>
14
33
  </ClerkProvider>
15
34
  );
@@ -33,11 +33,11 @@ function FaqItem({ q, a }: { q: string; a: string }) {
33
33
  return (
34
34
  <div className="border-b border-sand-200 dark:border-sand-800">
35
35
  <button
36
- className="flex w-full items-center justify-between py-4 text-left text-sand-900 transition-colors hover:text-[var(--brand-accent)] dark:text-sand-50"
36
+ className="flex w-full items-center justify-between py-4 text-left text-sand-900 transition-colors hover:text-[var(--brand-accent-text)] dark:text-sand-50"
37
37
  onClick={() => setOpen((o) => !o)}
38
38
  >
39
39
  <span className="font-medium">{q}</span>
40
- <span className={cn("ml-4 shrink-0 text-[var(--brand-accent)] transition-transform", open && "rotate-45")}>
40
+ <span className={cn("ml-4 shrink-0 text-[var(--brand-accent-text)] transition-transform", open && "rotate-45")}>
41
41
  +
42
42
  </span>
43
43
  </button>
@@ -29,19 +29,19 @@ export function StorefrontFooter() {
29
29
  href={buildSupportLink("whatsapp", { message: `Hi ${BRAND.name}!` })}
30
30
  target="_blank"
31
31
  rel="noopener noreferrer"
32
- className="inline-flex items-center gap-2 rounded-full bg-leaf-500 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-leaf-600"
32
+ className="inline-flex min-h-11 items-center gap-2 rounded-full bg-leaf-500 px-4 text-base font-semibold text-[#111111] transition-colors hover:bg-leaf-600"
33
33
  >
34
34
  WhatsApp
35
35
  </Link>
36
36
  <Link
37
37
  href={buildSupportLink("sms")}
38
- className="inline-flex items-center gap-2 rounded-full border border-sand-700 bg-sand-900/60 px-4 py-2 text-sm font-semibold text-sand-200 hover:border-[var(--brand-accent)]/40"
38
+ className="inline-flex min-h-11 items-center gap-2 rounded-full border border-sand-700 bg-sand-900/60 px-4 text-base font-semibold text-sand-200 hover:border-[var(--brand-accent)]/40"
39
39
  >
40
40
  SMS
41
41
  </Link>
42
42
  <Link
43
43
  href={`mailto:${BRAND.support.email}`}
44
- className="inline-flex items-center gap-2 rounded-full border border-sand-700 bg-sand-900/60 px-4 py-2 text-sm font-semibold text-sand-200 hover:border-[var(--brand-accent)]/40"
44
+ className="inline-flex min-h-11 items-center gap-2 rounded-full border border-sand-700 bg-sand-900/60 px-4 text-base font-semibold text-sand-200 hover:border-[var(--brand-accent)]/40"
45
45
  >
46
46
  Email
47
47
  </Link>
@@ -49,13 +49,13 @@ export function StorefrontFooter() {
49
49
  </div>
50
50
  {Object.entries(FOOTER_LINKS).map(([section, links]) => (
51
51
  <div key={section}>
52
- <p className="mb-3 text-xs font-semibold uppercase tracking-wider text-sand-500">
52
+ <p className="mb-3 text-xs font-semibold uppercase tracking-wider text-[#9C9480]">
53
53
  {section}
54
54
  </p>
55
55
  <ul className="space-y-2">
56
56
  {links.map((link) => (
57
57
  <li key={link.href}>
58
- <Link href={link.href} className="text-sm text-sand-400 transition-colors hover:text-sand-50">
58
+ <Link href={link.href} className="inline-flex min-h-11 items-center text-base text-sand-400 transition-colors hover:text-sand-50">
59
59
  {link.label}
60
60
  </Link>
61
61
  </li>
@@ -65,10 +65,10 @@ export function StorefrontFooter() {
65
65
  ))}
66
66
  </div>
67
67
  <div className="flex flex-col items-start justify-between gap-2 border-t border-sand-800 pt-6 md:flex-row md:items-center">
68
- <p className="text-xs text-sand-600">
68
+ <p className="text-xs text-[#A39A86]">
69
69
  © {new Date().getFullYear()} {BRAND.legalName}. All rights reserved.
70
70
  </p>
71
- <p className="text-xs text-sand-600">{BRAND.support.hoursLocal}</p>
71
+ <p className="text-xs text-[#A39A86]">{BRAND.support.hoursLocal}</p>
72
72
  </div>
73
73
  </div>
74
74
  </footer>
@@ -4,21 +4,16 @@ import brand from "@/brand.config";
4
4
  export function FinalCTA() {
5
5
  return (
6
6
  <section className="px-4 py-20">
7
- <div
8
- className="mx-auto max-w-6xl overflow-hidden rounded-[2rem] px-8 py-14 text-center md:px-16"
9
- style={{
10
- background: `linear-gradient(135deg, color-mix(in srgb, var(--brand-accent) 88%, #1A1812), var(--brand-accent-dark))`,
11
- }}
12
- >
13
- <h2 className="text-3xl font-semibold tracking-tight text-white md:text-4xl">
7
+ <div className="mx-auto max-w-6xl overflow-hidden rounded-[2rem] bg-[var(--brand-accent)] px-8 py-14 text-center text-[var(--brand-on-accent)] md:px-16">
8
+ <h2 className="text-3xl font-semibold tracking-tight text-[var(--brand-on-accent)] md:text-4xl">
14
9
  Travel light. Land connected.
15
10
  </h2>
16
- <p className="mx-auto mt-3 max-w-lg text-sm text-white/85">
11
+ <p className="mx-auto mt-3 max-w-lg text-sm text-[var(--brand-on-accent)]">
17
12
  {brand.tagline}
18
13
  </p>
19
14
  <Link
20
15
  href="/shop"
21
- className="mt-8 inline-flex rounded-full bg-white px-7 py-3 text-sm font-semibold text-sand-900 hover:bg-sand-50"
16
+ className="mt-8 inline-flex min-h-11 items-center rounded-full bg-sand-50 px-7 text-base font-semibold text-[#111111] hover:bg-white"
22
17
  >
23
18
  Browse plans
24
19
  </Link>
@@ -1,6 +1,7 @@
1
1
  import Image from "next/image";
2
2
  import Link from "next/link";
3
3
  import brand from "@/brand.config";
4
+ import { filledAccentClass } from "@/lib/tap-target";
4
5
 
5
6
  const STATS = [
6
7
  { value: "200+", label: "countries" },
@@ -25,7 +26,7 @@ export function HeroSection() {
25
26
  </p>
26
27
  <h1 className="font-display text-4xl font-semibold tracking-tight text-sand-900 sm:text-5xl md:text-6xl dark:text-sand-50">
27
28
  Stay connected,
28
- <span className="block" style={{ color: "var(--brand-accent)" }}>
29
+ <span className="block" style={{ color: "var(--brand-accent-text)" }}>
29
30
  everywhere.
30
31
  </span>
31
32
  </h1>
@@ -33,16 +34,12 @@ export function HeroSection() {
33
34
  {brand.tagline} Instant QR activation. No physical cards. No roaming surprises.
34
35
  </p>
35
36
  <div className="mt-8 flex flex-wrap items-center gap-3">
36
- <Link
37
- href="/shop"
38
- className="rounded-full px-7 py-3 text-sm font-semibold text-white transition-opacity hover:opacity-90"
39
- style={{ background: "var(--brand-accent)" }}
40
- >
37
+ <Link href="/shop" className={filledAccentClass}>
41
38
  Browse plans
42
39
  </Link>
43
40
  <Link
44
41
  href="/#how-it-works"
45
- className="rounded-full border border-sand-300 bg-white px-7 py-3 text-sm font-semibold text-sand-800 transition-colors hover:border-sand-400 dark:border-sand-700 dark:bg-sand-900 dark:text-sand-100"
42
+ className="inline-flex min-h-11 items-center rounded-full border border-sand-300 bg-white px-7 text-base font-semibold text-sand-800 transition-colors hover:border-sand-400 dark:border-sand-700 dark:bg-sand-900 dark:text-sand-100"
46
43
  >
47
44
  How it works
48
45
  </Link>
@@ -30,7 +30,7 @@ export function HowItWorks() {
30
30
  key={step.n}
31
31
  className="rounded-2xl border border-sand-200 bg-white p-6 dark:border-sand-800 dark:bg-sand-950"
32
32
  >
33
- <p className="text-sm font-semibold" style={{ color: "var(--brand-accent)" }}>
33
+ <p className="text-sm font-semibold" style={{ color: "var(--brand-accent-text)" }}>
34
34
  {step.n}
35
35
  </p>
36
36
  <h3 className="mt-3 text-lg font-semibold text-sand-900 dark:text-sand-50">{step.title}</h3>
@@ -3,6 +3,7 @@
3
3
  import Link from "next/link";
4
4
  import { motion } from "framer-motion";
5
5
  import { cn } from "@/vendor/ui/cn";
6
+ import { formatPrice } from "@/vendor/geo";
6
7
  import type { SkuTemplate } from "@/vendor/carrier/types";
7
8
 
8
9
  interface PlanCardProps {
@@ -19,7 +20,8 @@ interface PlanCardProps {
19
20
  }
20
21
 
21
22
  export function PlanCard({ plan, href, onSelect, featured, selected }: PlanCardProps) {
22
- const destination = href ?? (onSelect ? undefined : `/checkout/${plan.id}`);
23
+ const unavailable = plan.price_unavailable === true;
24
+ const destination = unavailable ? undefined : href ?? (onSelect ? undefined : `/checkout/${plan.id}`);
23
25
 
24
26
  function handleSelect() {
25
27
  onSelect?.(plan);
@@ -29,7 +31,7 @@ export function PlanCard({ plan, href, onSelect, featured, selected }: PlanCardP
29
31
  <>
30
32
  {featured && (
31
33
  <span
32
- className="absolute left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 rounded-full px-3 py-0.5 text-xs font-semibold text-white"
34
+ className="absolute left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 rounded-full px-3 py-0.5 text-xs font-semibold text-[var(--brand-on-accent)]"
33
35
  style={{ background: "var(--brand-accent)" }}
34
36
  >
35
37
  Most popular
@@ -37,39 +39,47 @@ export function PlanCard({ plan, href, onSelect, featured, selected }: PlanCardP
37
39
  )}
38
40
  <div>
39
41
  <p className="font-semibold text-sand-900 dark:text-sand-50">{plan.name}</p>
40
- <p className="mt-0.5 text-sm text-sand-500">{plan.description}</p>
42
+ <p className="mt-0.5 text-sm text-sand-700 dark:text-sand-200">{plan.description}</p>
41
43
  </div>
42
44
  <div className="flex items-baseline gap-1">
43
- <span className="text-3xl font-bold text-sand-900 dark:text-sand-50">
44
- €{(plan.price_cents / 100).toFixed(2)}
45
- </span>
46
- <span className="text-sm text-sand-500">one-time</span>
45
+ {plan.price_unavailable ? (
46
+ <span className="text-3xl font-bold text-sand-900 dark:text-sand-50">Unavailable</span>
47
+ ) : (
48
+ <span
49
+ className="text-3xl font-bold text-sand-900 dark:text-sand-50"
50
+ data-unit-amount={plan.price_cents}
51
+ data-currency={plan.currency}
52
+ >
53
+ {formatPrice(plan.price_cents, plan.currency)}
54
+ </span>
55
+ )}
56
+ <span className="text-sm text-sand-700 dark:text-sand-200">one-time</span>
47
57
  </div>
48
58
  <ul className="space-y-1.5 text-sm text-sand-700 dark:text-sand-300">
49
59
  {plan.features.map((f) => (
50
60
  <li key={f} className="flex items-center gap-2">
51
- <span style={{ color: "var(--brand-accent)" }}>✓</span>
61
+ <span style={{ color: "var(--brand-accent-text)" }}>✓</span>
52
62
  {f}
53
63
  </li>
54
64
  ))}
55
65
  </ul>
56
66
  <span
57
67
  className={cn(
58
- "mt-auto block rounded-full py-2 text-center text-sm font-semibold transition-colors",
68
+ "mt-auto inline-flex min-h-11 items-center justify-center rounded-full px-6 py-2 text-center text-base font-semibold transition-colors",
59
69
  featured || selected
60
- ? "text-white"
70
+ ? "text-[var(--brand-on-accent)]"
61
71
  : "border border-sand-300 text-sand-900 hover:bg-sand-100 dark:border-sand-700 dark:text-sand-50 dark:hover:bg-sand-800",
62
72
  )}
63
- style={featured || selected ? { background: "var(--brand-accent)" } : undefined}
73
+ style={featured || selected ? { background: "var(--brand-accent)", color: "var(--brand-on-accent)" } : undefined}
64
74
  >
65
- {selected ? "Selected" : "Select plan"}
75
+ {unavailable ? "Unavailable" : selected ? "Selected" : "Select plan"}
66
76
  </span>
67
77
  </>
68
78
  );
69
79
 
70
80
  const cardClass = cn(
71
81
  "relative flex flex-col gap-4 rounded-2xl border p-6 pt-8 transition-colors",
72
- selected && "ring-2 ring-[var(--brand-accent)] ring-offset-2 ring-offset-sand-50 dark:ring-offset-sand-950",
82
+ selected && "ring-2 ring-[var(--brand-focus)] ring-offset-2 ring-offset-sand-50 dark:ring-offset-sand-950",
73
83
  featured
74
84
  ? "border-[var(--brand-accent)] bg-[color-mix(in_srgb,var(--brand-accent)_6%,white)] dark:bg-[color-mix(in_srgb,var(--brand-accent)_12%,#0F0E0A)]"
75
85
  : "border-sand-200 bg-white hover:border-sand-300 dark:border-sand-800 dark:bg-sand-900 dark:hover:border-sand-700",