@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.
- package/dist/{chunk-6OAZJBLA.js → chunk-4XUAUQ3U.js} +67 -7
- package/dist/chunk-4XUAUQ3U.js.map +1 -0
- package/dist/cli.js +55 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.js +66 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/storefront/CLERK.md +23 -0
- package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +29 -22
- package/templates/storefront/src/app/activate/[orderId]/page.tsx +2 -2
- package/templates/storefront/src/app/api/checkout/claim/route.ts +7 -6
- package/templates/storefront/src/app/api/checkout/guest/route.ts +72 -17
- package/templates/storefront/src/app/api/orders/[orderId]/route.ts +2 -2
- package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +51 -25
- package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +165 -131
- package/templates/storefront/src/app/checkout/success/FocusHeading.tsx +20 -0
- package/templates/storefront/src/app/checkout/success/page.tsx +136 -15
- package/templates/storefront/src/app/dashboard/EsimControls.tsx +20 -24
- package/templates/storefront/src/app/dashboard/page.tsx +43 -23
- package/templates/storefront/src/app/globals.css +44 -5
- package/templates/storefront/src/app/help/page.tsx +2 -2
- package/templates/storefront/src/app/layout.tsx +5 -3
- package/templates/storefront/src/app/page.tsx +27 -7
- package/templates/storefront/src/app/shop/ShopClient.tsx +106 -72
- package/templates/storefront/src/app/shop/page.tsx +7 -2
- package/templates/storefront/src/app/sign-in/[[...sign-in]]/StorefrontSignInClient.tsx +297 -0
- package/templates/storefront/src/app/sign-in/[[...sign-in]]/page.tsx +13 -14
- package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +31 -28
- package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +5 -2
- package/templates/storefront/src/brand.config.ts +16 -0
- package/templates/storefront/src/components/Providers.tsx +20 -1
- package/templates/storefront/src/components/faq/FaqSection.tsx +2 -2
- package/templates/storefront/src/components/footer/StorefrontFooter.tsx +7 -7
- package/templates/storefront/src/components/landing/FinalCTA.tsx +4 -9
- package/templates/storefront/src/components/landing/HeroSection.tsx +4 -7
- package/templates/storefront/src/components/landing/HowItWorks.tsx +1 -1
- package/templates/storefront/src/components/landing/PlanCard.tsx +23 -13
- package/templates/storefront/src/components/landing/TierPlanBoard.tsx +237 -0
- package/templates/storefront/src/components/nav/StorefrontNav.tsx +8 -11
- package/templates/storefront/src/components/shop/CountrySearch.tsx +94 -0
- package/templates/storefront/src/components/theme/BrandStyles.tsx +2 -2
- package/templates/storefront/src/components/theme/ThemeToggle.tsx +1 -1
- package/templates/storefront/src/components/theme/brand-accent-css.ts +21 -0
- package/templates/storefront/src/components/theme/brand-style.ts +88 -0
- package/templates/storefront/src/components/theme/clerk-appearance.ts +18 -2
- package/templates/storefront/src/lib/buyer-auth-copy.ts +83 -0
- package/templates/storefront/src/lib/buyer-error.ts +32 -0
- package/templates/storefront/src/lib/buyer-session.ts +41 -0
- package/templates/storefront/src/lib/catalog-countries.ts +179 -0
- package/templates/storefront/src/lib/checkout-client-error.ts +28 -0
- package/templates/storefront/src/lib/checkout-copy.ts +89 -0
- package/templates/storefront/src/lib/checkout-order-claim.ts +8 -7
- package/templates/storefront/src/lib/checkout-return.ts +18 -0
- package/templates/storefront/src/lib/clerk-instance.ts +89 -0
- package/templates/storefront/src/lib/order-reference.ts +15 -0
- package/templates/storefront/src/lib/request-origin.ts +28 -0
- package/templates/storefront/src/lib/sanitize-auth-redirect.ts +20 -4
- package/templates/storefront/src/lib/tap-target.ts +17 -0
- package/templates/storefront/src/lib/verify-checkout-session.ts +118 -19
- package/templates/storefront/src/middleware.ts +11 -0
- package/templates/storefront/src/vendor/carrier/catalog-guard.ts +9 -5
- package/templates/storefront/src/vendor/carrier/client.ts +22 -4
- package/templates/storefront/src/vendor/carrier/country-offers.ts +106 -0
- package/templates/storefront/src/vendor/carrier/stripe-price.ts +134 -0
- package/templates/storefront/src/vendor/carrier/tiers.ts +161 -0
- package/templates/storefront/src/vendor/carrier/types.ts +22 -0
- package/templates/storefront/src/vendor/geo/index.ts +15 -4
- 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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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-
|
|
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-
|
|
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=
|
|
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-
|
|
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=
|
|
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-
|
|
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=
|
|
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-
|
|
137
|
+
<p className="mt-4 text-center text-base text-sand-500">
|
|
137
138
|
Already have an account?{" "}
|
|
138
|
-
<Link href=
|
|
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-
|
|
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-
|
|
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=
|
|
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-
|
|
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=
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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="
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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",
|