@carrierllc/mcp 0.13.21 → 0.13.24
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-IUYX4NKC.js → chunk-YQQUV7AE.js} +9 -6
- package/dist/{chunk-IUYX4NKC.js.map → chunk-YQQUV7AE.js.map} +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/storefront/eslint.config.mjs +8 -1
- package/templates/storefront/package-lock.json +532 -694
- package/templates/storefront/package.json +20 -12
- package/templates/storefront/pnpm-lock.yaml +1321 -1504
- package/templates/storefront/pnpm-workspace.yaml +7 -0
- package/templates/storefront/src/app/api/checkout/guest/route.ts +174 -114
- package/templates/storefront/src/app/api/orders/[orderId]/cancel/route.ts +11 -6
- package/templates/storefront/src/app/api/orders/[orderId]/top-up-options/route.ts +7 -4
- package/templates/storefront/src/app/api/orders/[orderId]/usage/route.ts +7 -4
- package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +22 -17
- package/templates/storefront/src/app/contact/page.tsx +3 -3
- package/templates/storefront/src/app/dashboard/EsimControls.tsx +4 -4
- package/templates/storefront/src/app/help/page.tsx +1 -1
- package/templates/storefront/src/app/shop/ShopClient.tsx +1 -1
- package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +3 -2
- package/templates/storefront/src/app/sitemap.ts +1 -0
- package/templates/storefront/src/components/checkout/EmbeddedStripeCheckout.tsx +4 -2
- package/templates/storefront/src/components/faq/FaqSection.tsx +1 -1
- package/templates/storefront/src/components/footer/StorefrontFooter.tsx +2 -2
- package/templates/storefront/src/components/landing/FinalCTA.tsx +1 -1
- package/templates/storefront/src/components/landing/HeroSection.tsx +6 -2
- package/templates/storefront/src/components/nav/StorefrontNav.tsx +2 -1
- package/templates/storefront/src/components/theme/theme-provider.tsx +28 -17
- package/templates/storefront/src/lib/checkout-order-claim.ts +5 -14
- package/templates/storefront/src/lib/order-route-guard.ts +34 -0
- package/templates/storefront/src/middleware.ts +5 -0
- package/templates/storefront/src/vendor/carrier/types.ts +4 -0
- package/templates/storefront/src/vendor/geo/index.ts +5 -5
- package/templates/storefront/src/vendor/ui/brand.ts +1 -1
|
@@ -69,7 +69,7 @@ export function FaqSection() {
|
|
|
69
69
|
<FaqItem key={faq.q} q={faq.q} a={faq.a} />
|
|
70
70
|
))}
|
|
71
71
|
<p className="mt-8 text-center text-sm text-sand-500">
|
|
72
|
-
Still stuck? WhatsApp {brand.name}
|
|
72
|
+
Still stuck? WhatsApp {brand.name}.
|
|
73
73
|
</p>
|
|
74
74
|
</div>
|
|
75
75
|
</section>
|
|
@@ -31,7 +31,7 @@ export function StorefrontFooter() {
|
|
|
31
31
|
rel="noopener noreferrer"
|
|
32
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"
|
|
33
33
|
>
|
|
34
|
-
WhatsApp
|
|
34
|
+
WhatsApp
|
|
35
35
|
</Link>
|
|
36
36
|
<Link
|
|
37
37
|
href={buildSupportLink("sms")}
|
|
@@ -68,7 +68,7 @@ export function StorefrontFooter() {
|
|
|
68
68
|
<p className="text-xs text-sand-600">
|
|
69
69
|
© {new Date().getFullYear()} {BRAND.legalName}. All rights reserved.
|
|
70
70
|
</p>
|
|
71
|
-
<p className="text-xs text-sand-600">
|
|
71
|
+
<p className="text-xs text-sand-600">{BRAND.support.hoursLocal}</p>
|
|
72
72
|
</div>
|
|
73
73
|
</div>
|
|
74
74
|
</footer>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import Image from "next/image";
|
|
1
2
|
import Link from "next/link";
|
|
2
3
|
import brand from "@/brand.config";
|
|
3
4
|
|
|
4
5
|
const STATS = [
|
|
5
6
|
{ value: "200+", label: "countries" },
|
|
6
7
|
{ value: "<30s", label: "to activate" },
|
|
7
|
-
{ value: "
|
|
8
|
+
{ value: "1 business day", label: "reply target" },
|
|
8
9
|
];
|
|
9
10
|
|
|
10
11
|
export function HeroSection() {
|
|
@@ -67,9 +68,12 @@ export function HeroSection() {
|
|
|
67
68
|
key={item.label}
|
|
68
69
|
className="relative overflow-hidden rounded-2xl border border-sand-200 bg-sand-100 shadow-sm dark:border-sand-800"
|
|
69
70
|
>
|
|
70
|
-
<
|
|
71
|
+
<Image
|
|
71
72
|
src={item.src}
|
|
72
73
|
alt={item.label}
|
|
74
|
+
width={640}
|
|
75
|
+
height={480}
|
|
76
|
+
unoptimized
|
|
73
77
|
className="aspect-[4/3] w-full object-cover"
|
|
74
78
|
/>
|
|
75
79
|
<figcaption className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-sand-950/70 to-transparent px-3 py-2 text-xs font-medium text-white">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
4
5
|
import Link from "next/link";
|
|
5
6
|
import { usePathname } from "next/navigation";
|
|
6
7
|
import { SignedIn, SignedOut, UserButton } from "@clerk/nextjs";
|
|
@@ -22,7 +23,7 @@ export function StorefrontNav() {
|
|
|
22
23
|
<header className="fixed inset-x-0 top-0 z-50 border-b border-sand-200/80 bg-sand-50/80 backdrop-blur-md dark:border-sand-800 dark:bg-sand-950/80">
|
|
23
24
|
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-4">
|
|
24
25
|
<Link href="/" className="flex items-center gap-2 text-base font-semibold tracking-tight text-sand-900 dark:text-sand-50">
|
|
25
|
-
<
|
|
26
|
+
<Image src="/brand/logo.svg" alt="" width={32} height={32} unoptimized className="size-8 rounded-lg" />
|
|
26
27
|
{brand.name}
|
|
27
28
|
</Link>
|
|
28
29
|
|
|
@@ -29,30 +29,41 @@ function applyToDocument(theme: Theme) {
|
|
|
29
29
|
root.style.colorScheme = theme;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function subscribePreference(onChange: () => void) {
|
|
33
|
+
window.addEventListener("storage", onChange);
|
|
34
|
+
return () => window.removeEventListener("storage", onChange);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readPreference(): ThemePreference {
|
|
38
|
+
try {
|
|
39
|
+
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
40
|
+
if (stored === "light" || stored === "dark" || stored === "system") return stored;
|
|
41
|
+
} catch {
|
|
42
|
+
/* private mode */
|
|
43
|
+
}
|
|
44
|
+
return "light";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function serverPreference(): ThemePreference {
|
|
48
|
+
return "light";
|
|
49
|
+
}
|
|
50
|
+
|
|
32
51
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
33
|
-
const
|
|
34
|
-
|
|
52
|
+
const storedPreference = React.useSyncExternalStore(
|
|
53
|
+
subscribePreference, readPreference, serverPreference,
|
|
54
|
+
);
|
|
55
|
+
// Keep user changes usable even when private mode prevents persistence.
|
|
56
|
+
const [override, setPreferenceState] = React.useState<ThemePreference | null>(null);
|
|
57
|
+
const preference = override ?? storedPreference;
|
|
58
|
+
const theme = resolve(preference);
|
|
35
59
|
|
|
36
60
|
React.useEffect(() => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (stored === "light" || stored === "dark" || stored === "system") {
|
|
40
|
-
setPreferenceState(stored);
|
|
41
|
-
const next = resolve(stored);
|
|
42
|
-
setTheme(next);
|
|
43
|
-
applyToDocument(next);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
/* private mode */
|
|
48
|
-
}
|
|
49
|
-
applyToDocument("light");
|
|
50
|
-
}, []);
|
|
61
|
+
applyToDocument(theme);
|
|
62
|
+
}, [theme]);
|
|
51
63
|
|
|
52
64
|
const setPreference = React.useCallback((pref: ThemePreference) => {
|
|
53
65
|
setPreferenceState(pref);
|
|
54
66
|
const next = resolve(pref);
|
|
55
|
-
setTheme(next);
|
|
56
67
|
applyToDocument(next);
|
|
57
68
|
try {
|
|
58
69
|
window.localStorage.setItem(STORAGE_KEY, pref);
|
|
@@ -105,18 +105,16 @@ function sessionPurchaserEmail(session: StripeSession): string | null {
|
|
|
105
105
|
/**
|
|
106
106
|
* Does this Clerk user own the address that paid for the session?
|
|
107
107
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* address, because an unverified one is a string the claimant typed.
|
|
108
|
+
* Every live claim needs a verified paying address, including the first claim.
|
|
109
|
+
* An unverified address is just a string the claimant typed; knowing a session
|
|
110
|
+
* id never grants ownership.
|
|
112
111
|
*/
|
|
113
112
|
async function userMatchesSessionPurchaser(
|
|
114
113
|
session: StripeSession,
|
|
115
114
|
userId: string,
|
|
116
|
-
opts: { requireVerified?: boolean } = {},
|
|
117
115
|
): Promise<boolean> {
|
|
118
116
|
const purchaserEmail = sessionPurchaserEmail(session);
|
|
119
|
-
if (!purchaserEmail) return
|
|
117
|
+
if (!purchaserEmail) return false;
|
|
120
118
|
|
|
121
119
|
const client = await clerkClient();
|
|
122
120
|
const user = await client.users.getUser(userId);
|
|
@@ -124,7 +122,7 @@ async function userMatchesSessionPurchaser(
|
|
|
124
122
|
return user.emailAddresses.some(
|
|
125
123
|
(address) =>
|
|
126
124
|
address.emailAddress.toLowerCase() === normalizedPurchaserEmail &&
|
|
127
|
-
|
|
125
|
+
address.verification?.status === "verified",
|
|
128
126
|
);
|
|
129
127
|
}
|
|
130
128
|
|
|
@@ -191,13 +189,6 @@ export async function claimCheckoutOrder(
|
|
|
191
189
|
* Without that, anyone who learned a session id could take someone
|
|
192
190
|
* else's order by typing their email into a new account.
|
|
193
191
|
*/
|
|
194
|
-
const ownsPayingAddress = await userMatchesSessionPurchaser(session, userId, {
|
|
195
|
-
requireVerified: true,
|
|
196
|
-
});
|
|
197
|
-
if (!ownsPayingAddress) {
|
|
198
|
-
return { ok: false, status: 403, error: "This order is linked to another account" };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
192
|
const moved = await setStripeClaimedBy(sessionId, userId);
|
|
202
193
|
if (!moved) {
|
|
203
194
|
return { ok: false, status: 502, error: "Failed to claim order" };
|
|
@@ -45,6 +45,40 @@ export async function guardOrderRoute(
|
|
|
45
45
|
return { ok: true, userId, orderId };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Server-side fulfilment call for the guarded proxy actions. The tenant comes
|
|
50
|
+
* only from deployment configuration, never from the caller's query/body.
|
|
51
|
+
* Keep the unscoped behavior for standalone shops; hosted orders require the
|
|
52
|
+
* slug upstream, which refuses a missing or mismatched tenant with 404.
|
|
53
|
+
*/
|
|
54
|
+
export async function orderActionCall<T>(
|
|
55
|
+
orderId: string,
|
|
56
|
+
action: "usage" | "top-up-options" | "cancel",
|
|
57
|
+
): Promise<{ ok: true; data: T } | { ok: false; reason: OrderLookupError }> {
|
|
58
|
+
const base = (process.env.NEXT_PUBLIC_CARRIER_API_URL ?? "").replace(/\/+$/, "");
|
|
59
|
+
if (!base) return { ok: false, reason: "unconfigured" };
|
|
60
|
+
if (!/^[A-Za-z0-9_-]{1,256}$/.test(orderId)) return { ok: false, reason: "not-found" };
|
|
61
|
+
|
|
62
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
63
|
+
const key = process.env.CARRIER_API_KEY;
|
|
64
|
+
if (key) headers.Authorization = `Bearer ${key}`;
|
|
65
|
+
try {
|
|
66
|
+
const url = new URL(`${base}/api/orders/${encodeURIComponent(orderId)}/${action}`);
|
|
67
|
+
const slug = process.env.NEXT_PUBLIC_STOREFRONT_SLUG;
|
|
68
|
+
if (slug) url.searchParams.set("storefront", slug);
|
|
69
|
+
const response = await fetch(url.toString(), {
|
|
70
|
+
method: action === "cancel" ? "POST" : "GET",
|
|
71
|
+
headers,
|
|
72
|
+
cache: "no-store",
|
|
73
|
+
});
|
|
74
|
+
if (response.status === 404) return { ok: false, reason: "not-found" };
|
|
75
|
+
if (!response.ok) return { ok: false, reason: "upstream-error" };
|
|
76
|
+
return { ok: true, data: (await response.json()) as T };
|
|
77
|
+
} catch {
|
|
78
|
+
return { ok: false, reason: "network-error" };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
48
82
|
/**
|
|
49
83
|
* One fulfilment failure, as customer copy plus a status.
|
|
50
84
|
*
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Keep the middleware.ts convention (not Next 16's proxy.ts). proxy.ts always
|
|
2
|
+
// runs on the Node.js runtime, and @opennextjs/cloudflare rejects Node.js
|
|
3
|
+
// middleware ("Node.js middleware is not currently supported"), which broke
|
|
4
|
+
// the template's cf:build in the npm Release job. middleware.ts still runs on
|
|
5
|
+
// the Edge runtime that the Cloudflare/OpenNext build supports.
|
|
1
6
|
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
|
|
2
7
|
|
|
3
8
|
const isPublicRoute = createRouteMatcher([
|
|
@@ -6,6 +6,8 @@ export interface SkuTemplate {
|
|
|
6
6
|
name: string;
|
|
7
7
|
description: string;
|
|
8
8
|
dataGb: number;
|
|
9
|
+
/** Only an explicit true value proves an unlimited allowance. */
|
|
10
|
+
unlimited?: boolean;
|
|
9
11
|
validityDays: number;
|
|
10
12
|
price_cents: number;
|
|
11
13
|
currency: string;
|
|
@@ -86,6 +88,8 @@ export interface TopUpOption {
|
|
|
86
88
|
id: TemplateId;
|
|
87
89
|
name: string;
|
|
88
90
|
dataGb: number;
|
|
91
|
+
/** Only an explicit true value proves an unlimited allowance. */
|
|
92
|
+
unlimited?: boolean;
|
|
89
93
|
validityDays: number;
|
|
90
94
|
price_cents: number;
|
|
91
95
|
currency: string;
|
|
@@ -22,18 +22,18 @@ export function formatPrice(cents: number, currency = "EUR", locale = "en"): str
|
|
|
22
22
|
/**
|
|
23
23
|
* A plan's data allowance as a shopper reads it.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
* sold, and printing a zero next to a price reads as a broken row.
|
|
25
|
+
* An explicit catalog flag proves unlimited; zero or missing data does not.
|
|
27
26
|
*/
|
|
28
|
-
export function formatDataAllowance(dataGb: number): string {
|
|
29
|
-
if (
|
|
27
|
+
export function formatDataAllowance(dataGb: number, unlimited = false): string {
|
|
28
|
+
if (unlimited === true) return "Unlimited data";
|
|
29
|
+
if (!Number.isFinite(dataGb) || dataGb <= 0) return "Allowance not specified";
|
|
30
30
|
if (dataGb < 1) return `${Math.round(dataGb * 1024)} MB data`;
|
|
31
31
|
return `${Number.isInteger(dataGb) ? dataGb : dataGb.toFixed(1)} GB data`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/** A plan's validity window, singular-aware. */
|
|
35
35
|
export function formatValidity(validityDays: number): string {
|
|
36
|
-
if (!Number.isFinite(validityDays) || validityDays <= 0) return "
|
|
36
|
+
if (!Number.isFinite(validityDays) || validityDays <= 0) return "Validity not specified";
|
|
37
37
|
if (validityDays === 1) return "1 day";
|
|
38
38
|
if (validityDays % 7 === 0 && validityDays >= 7) {
|
|
39
39
|
const weeks = validityDays / 7;
|