@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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speed tiers on the shop.
|
|
3
|
+
*
|
|
4
|
+
* Calm and Cruise come from the plans that were loaded. Lightning stays
|
|
5
|
+
* hidden unless this tenant turned the tier on and every Lightning plan has
|
|
6
|
+
* a real Stripe Price id. Fixture plans are never shown, and a production
|
|
7
|
+
* build does not contain them.
|
|
8
|
+
*/
|
|
9
|
+
import { isStripePriceId } from "./stripe-price";
|
|
10
|
+
import type { SkuTemplate } from "./types";
|
|
11
|
+
|
|
12
|
+
export const SPEED_TIERS = ["Calm", "Cruise", "Lightning"] as const;
|
|
13
|
+
export type SpeedTier = (typeof SPEED_TIERS)[number];
|
|
14
|
+
|
|
15
|
+
/** Buyer copy. No fixture wording. */
|
|
16
|
+
export const TIER_HELP_COPY = "Pick a data size and how long it lasts.";
|
|
17
|
+
|
|
18
|
+
export function tierEmptyCopy(tier: SpeedTier): string {
|
|
19
|
+
return `No ${tier} plans yet.`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function devLightningFixture(dataGb: number, validityDays: number, priceCents: number): SkuTemplate {
|
|
23
|
+
return {
|
|
24
|
+
id: `fixture-lightning-${dataGb}gb-${validityDays}d`,
|
|
25
|
+
name: "Lightning — test fixture",
|
|
26
|
+
description: "Test fixture. Not a production plan.",
|
|
27
|
+
dataGb,
|
|
28
|
+
validityDays,
|
|
29
|
+
price_cents: priceCents,
|
|
30
|
+
currency: "EUR",
|
|
31
|
+
fixture: true,
|
|
32
|
+
regions: [],
|
|
33
|
+
features: [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Checkout-refusal samples for tests. Empty in production so the strings and
|
|
39
|
+
* prices cannot be bundled into a buyer build.
|
|
40
|
+
*/
|
|
41
|
+
export const LIGHTNING_TEST_FIXTURES: readonly SkuTemplate[] =
|
|
42
|
+
process.env.NODE_ENV === "production"
|
|
43
|
+
? []
|
|
44
|
+
: [
|
|
45
|
+
devLightningFixture(1, 7, 101),
|
|
46
|
+
devLightningFixture(3, 7, 202),
|
|
47
|
+
devLightningFixture(5, 14, 303),
|
|
48
|
+
devLightningFixture(10, 30, 404),
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export function speedTierOf(plan: Pick<SkuTemplate, "name" | "id">): SpeedTier | null {
|
|
52
|
+
const haystack = `${plan.name} ${plan.id}`.toLowerCase();
|
|
53
|
+
for (const tier of SPEED_TIERS) {
|
|
54
|
+
if (new RegExp(`(^|[^a-z])${tier.toLowerCase()}([^a-z]|$)`).test(haystack)) return tier;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A plan invented for tests, whatever the tenant flag says. */
|
|
60
|
+
export function isFixturePlan(plan: Pick<SkuTemplate, "id" | "name" | "description" | "fixture">): boolean {
|
|
61
|
+
if (plan.fixture === true) return true;
|
|
62
|
+
if (plan.id.startsWith("fixture-")) return true;
|
|
63
|
+
const copy = `${plan.name} ${plan.description}`;
|
|
64
|
+
return /test fixture/i.test(copy);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TierCatalogOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Per-tenant switch. Off unless the tenant sets
|
|
70
|
+
* NEXT_PUBLIC_LIGHTNING_TIER=1. Real Stripe Price ids are still required.
|
|
71
|
+
*/
|
|
72
|
+
lightningEnabled?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TierGroup {
|
|
76
|
+
tier: SpeedTier;
|
|
77
|
+
plans: SkuTemplate[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface TierCatalogView {
|
|
81
|
+
tiers: TierGroup[];
|
|
82
|
+
untiered: SkuTemplate[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** True only when the tenant opted in and every Lightning plan has a Price id. */
|
|
86
|
+
export function lightningTierVisible(plans: SkuTemplate[], lightningEnabled: boolean): boolean {
|
|
87
|
+
if (lightningEnabled !== true) return false;
|
|
88
|
+
const lightning = plans.filter((plan) => speedTierOf(plan) === "Lightning" && !isFixturePlan(plan));
|
|
89
|
+
return lightning.length > 0 && lightning.every((plan) => isStripePriceId(plan.stripe_price_id));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Tiers a buyer can see. Lightning is omitted until it is configured. */
|
|
93
|
+
export function presentTierCatalog(plans: SkuTemplate[], options: TierCatalogOptions = {}): TierCatalogView {
|
|
94
|
+
const sellable = plans.filter((plan) => !isFixturePlan(plan));
|
|
95
|
+
const showLightning = lightningTierVisible(sellable, options.lightningEnabled === true);
|
|
96
|
+
const tiers = SPEED_TIERS.filter((tier) => tier !== "Lightning" || showLightning).map((tier) => ({
|
|
97
|
+
tier,
|
|
98
|
+
plans: sellable.filter((plan) => speedTierOf(plan) === tier),
|
|
99
|
+
}));
|
|
100
|
+
return {
|
|
101
|
+
tiers,
|
|
102
|
+
untiered: sellable.filter((plan) => speedTierOf(plan) === null),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The SKU a tier-level select action chooses: the current data and validity,
|
|
108
|
+
* which for an untouched tier is its first plan. One plan is enough.
|
|
109
|
+
*/
|
|
110
|
+
export function tierChoiceSku(plans: SkuTemplate[]): SkuTemplate | null {
|
|
111
|
+
return combinations(plans)[0] ?? null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function uniqueSorted(values: number[]): number[] {
|
|
115
|
+
const seen: number[] = [];
|
|
116
|
+
for (const value of values) {
|
|
117
|
+
if (!seen.includes(value)) seen.push(value);
|
|
118
|
+
}
|
|
119
|
+
return seen.sort((a, b) => a - b);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function dataChoices(plans: SkuTemplate[]): number[] {
|
|
123
|
+
return uniqueSorted(plans.map((plan) => plan.dataGb));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function validityChoices(plans: SkuTemplate[], dataGb: number): number[] {
|
|
127
|
+
return uniqueSorted(plans.filter((plan) => plan.dataGb === dataGb).map((plan) => plan.validityDays));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function skuFor(plans: SkuTemplate[], dataGb: number, validityDays: number): SkuTemplate | null {
|
|
131
|
+
return plans.find((plan) => plan.dataGb === dataGb && plan.validityDays === validityDays) ?? null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Every data × validity pair that exists. Missing pairs are not included. */
|
|
135
|
+
export function combinations(plans: SkuTemplate[]): SkuTemplate[] {
|
|
136
|
+
const chosen: SkuTemplate[] = [];
|
|
137
|
+
for (const dataGb of dataChoices(plans)) {
|
|
138
|
+
for (const validityDays of validityChoices(plans, dataGb)) {
|
|
139
|
+
const sku = skuFor(plans, dataGb, validityDays);
|
|
140
|
+
if (sku) chosen.push(sku);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return chosen;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Text a production render would show. Fixture prices and labels stay out. */
|
|
147
|
+
export function buyerFacingTierText(plans: SkuTemplate[], options: TierCatalogOptions = {}): string {
|
|
148
|
+
const view = presentTierCatalog(plans, options);
|
|
149
|
+
const lines: string[] = [TIER_HELP_COPY];
|
|
150
|
+
for (const group of view.tiers) {
|
|
151
|
+
lines.push(group.tier);
|
|
152
|
+
if (group.plans.length === 0) {
|
|
153
|
+
lines.push(tierEmptyCopy(group.tier));
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const sku of combinations(group.plans)) {
|
|
157
|
+
lines.push(sku.id, sku.name, sku.description, String(sku.price_cents), sku.currency);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return lines.join("\n");
|
|
161
|
+
}
|
|
@@ -11,6 +11,22 @@ export interface SkuTemplate {
|
|
|
11
11
|
validityDays: number;
|
|
12
12
|
price_cents: number;
|
|
13
13
|
currency: string;
|
|
14
|
+
/**
|
|
15
|
+
* Stripe Price id (`price_…`) that checkout charges. The page reads that
|
|
16
|
+
* Price's `unit_amount` and currency. Absent on sample plans, which must
|
|
17
|
+
* not be sold.
|
|
18
|
+
*/
|
|
19
|
+
stripe_price_id?: string;
|
|
20
|
+
/**
|
|
21
|
+
* The Stripe Price could not be read. The page must not show `price_cents`,
|
|
22
|
+
* which is only the catalog copy of an amount checkout does not charge.
|
|
23
|
+
*/
|
|
24
|
+
price_unavailable?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* True only for a Lightning option drawn from the test-fixture tier when
|
|
27
|
+
* the loaded catalog has no Lightning plan. Never a production SKU.
|
|
28
|
+
*/
|
|
29
|
+
fixture?: boolean;
|
|
14
30
|
regions: string[];
|
|
15
31
|
features: string[];
|
|
16
32
|
}
|
|
@@ -57,6 +73,12 @@ export interface EsimOrder {
|
|
|
57
73
|
expiresAt?: string;
|
|
58
74
|
/** Operator-supplied, customer-safe explanation when status is "failed". */
|
|
59
75
|
failureMessage?: string;
|
|
76
|
+
/** Gigabytes on the plan. Omitted when the plan is unlimited or unknown. */
|
|
77
|
+
dataGb?: number;
|
|
78
|
+
/** True only when the plan is explicitly unlimited. */
|
|
79
|
+
unlimited?: boolean;
|
|
80
|
+
/** Days the plan stays valid. Omitted when the plan does not say. */
|
|
81
|
+
validityDays?: number;
|
|
60
82
|
/**
|
|
61
83
|
* The plan is no longer billable. Set once a cancellation has been accepted,
|
|
62
84
|
* so the dashboard can stop offering to cancel it twice.
|
|
@@ -7,15 +7,26 @@ export function getCountryName(iso: string, locale = "en"): string {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/** Stripe zero-decimal currencies. `unit_amount` is already the major unit. */
|
|
11
|
+
const ZERO_DECIMAL = new Set([
|
|
12
|
+
"BIF", "CLP", "DJF", "GNF", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF",
|
|
13
|
+
"UGX", "VND", "VUV", "XAF", "XOF", "XPF",
|
|
14
|
+
]);
|
|
15
|
+
|
|
10
16
|
export function formatPrice(cents: number, currency = "EUR", locale = "en"): string {
|
|
17
|
+
const code = currency.toUpperCase();
|
|
18
|
+
const zeroDecimal = ZERO_DECIMAL.has(code);
|
|
19
|
+
const major = zeroDecimal ? cents : cents / 100;
|
|
20
|
+
const digits = zeroDecimal ? 0 : 2;
|
|
11
21
|
try {
|
|
12
22
|
return new Intl.NumberFormat(locale, {
|
|
13
23
|
style: "currency",
|
|
14
|
-
currency,
|
|
15
|
-
minimumFractionDigits:
|
|
16
|
-
|
|
24
|
+
currency: code,
|
|
25
|
+
minimumFractionDigits: digits,
|
|
26
|
+
maximumFractionDigits: digits,
|
|
27
|
+
}).format(major);
|
|
17
28
|
} catch {
|
|
18
|
-
return `${
|
|
29
|
+
return `${code} ${zeroDecimal ? String(major) : major.toFixed(digits)}`;
|
|
19
30
|
}
|
|
20
31
|
}
|
|
21
32
|
|