@forgecart/cli 2.202607190047.0 → 2.202607240817.0
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/src/commands/init.d.ts +2 -2
- package/dist/src/commands/init.js +5 -5
- package/dist/src/commands/init.js.map +1 -1
- package/package.json +1 -1
- package/templates/storefront/.env.example +1 -1
- package/templates/storefront/README.md +2 -2
- package/templates/storefront/next.config.js +16 -15
- package/templates/storefront/package.json +3 -2
- package/templates/storefront/src/app/%5F%5Ffc/track/route.ts +168 -0
- package/templates/storefront/src/app/layout.tsx +2 -2
- package/templates/storefront/src/app/page.tsx +15 -3
- package/templates/storefront/src/components/CartView.tsx +2 -0
- package/templates/storefront/src/components/ForgeTracker.tsx +267 -0
- package/templates/storefront/src/components/Header.tsx +1 -1
- package/templates/storefront/src/components/ProductCard.tsx +1 -0
- package/templates/storefront/src/components/ProductPurchase.tsx +1 -0
- package/templates/storefront/src/lib/cart-actions.ts +1 -1
- package/templates/storefront/src/lib/experiments.ts +335 -0
- package/templates/storefront/src/lib/forgecart.ts +3 -3
- package/templates/storefront/src/lib/track-forward.ts +158 -0
- package/templates/storefront/src/middleware.ts +199 -0
- package/templates/storefront/src/components/ForgeAnalytics.tsx +0 -128
|
@@ -30,7 +30,7 @@ export interface InitOptions {
|
|
|
30
30
|
* Serve-in-place mode: the target directory is ALREADY a scaffolded,
|
|
31
31
|
* dependency- and dev-compile-warmed storefront (materialized into the pod's
|
|
32
32
|
* `/workspace` from the image's pre-warmed tree). Write ONLY the per-channel
|
|
33
|
-
* config + `.env
|
|
33
|
+
* config + `.env` and skip the template copy entirely — copying would
|
|
34
34
|
* clobber the warm `.next` and bust the Turbopack dev cache.
|
|
35
35
|
*/
|
|
36
36
|
configOnly?: boolean;
|
|
@@ -40,7 +40,7 @@ export interface InitOptions {
|
|
|
40
40
|
*
|
|
41
41
|
* Resolves the target directory, scaffolds the storefront from the warmed
|
|
42
42
|
* template when one is available (see {@link resolveScaffoldPlan}) or the
|
|
43
|
-
* bundled template otherwise, writes the ForgeCart config and `.env
|
|
43
|
+
* bundled template otherwise, writes the ForgeCart config and `.env`, and
|
|
44
44
|
* prints next steps. Throws a clear Error on any failure (missing token,
|
|
45
45
|
* missing template, copy failure).
|
|
46
46
|
*/
|
|
@@ -123,8 +123,8 @@ function buildConfig(channelToken, options) {
|
|
|
123
123
|
storefrontPath: '.',
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
|
-
/** Build the `.env
|
|
127
|
-
function
|
|
126
|
+
/** Build the `.env` contents for the storefront SDK client. */
|
|
127
|
+
function buildEnv(channelToken, apiUrl) {
|
|
128
128
|
return [
|
|
129
129
|
`FORGECART_CHANNEL_TOKEN=${channelToken}`,
|
|
130
130
|
`FORGECART_SHOP_API_URL=${apiUrl}`,
|
|
@@ -133,7 +133,7 @@ function buildEnvLocal(channelToken, apiUrl) {
|
|
|
133
133
|
}
|
|
134
134
|
/**
|
|
135
135
|
* Write the per-channel config the storefront reads at runtime: `.forgecart/
|
|
136
|
-
* config.json` (channel token, api url, app flag) and `.env
|
|
136
|
+
* config.json` (channel token, api url, app flag) and `.env` (the SDK
|
|
137
137
|
* client env). Shared by the full scaffold and the `--config-only` serve-in-place
|
|
138
138
|
* path, so both write byte-identical config — the only difference is whether the
|
|
139
139
|
* template tree was copied first.
|
|
@@ -142,7 +142,7 @@ async function writeChannelConfig(targetDir, channelToken, options) {
|
|
|
142
142
|
const configDir = join(targetDir, '.forgecart');
|
|
143
143
|
await mkdir(configDir, { recursive: true });
|
|
144
144
|
await writeFile(join(configDir, 'config.json'), `${JSON.stringify(buildConfig(channelToken, options), null, 2)}\n`, 'utf8');
|
|
145
|
-
await writeFile(join(targetDir, '.env
|
|
145
|
+
await writeFile(join(targetDir, '.env'), buildEnv(channelToken, options.apiUrl), 'utf8');
|
|
146
146
|
}
|
|
147
147
|
/** Print the post-scaffold next steps. */
|
|
148
148
|
function printNextSteps(targetDir, application, depsInstalled) {
|
|
@@ -179,7 +179,7 @@ function printConfigOnly(targetDir) {
|
|
|
179
179
|
*
|
|
180
180
|
* Resolves the target directory, scaffolds the storefront from the warmed
|
|
181
181
|
* template when one is available (see {@link resolveScaffoldPlan}) or the
|
|
182
|
-
* bundled template otherwise, writes the ForgeCart config and `.env
|
|
182
|
+
* bundled template otherwise, writes the ForgeCart config and `.env`, and
|
|
183
183
|
* prints next steps. Throws a clear Error on any failure (missing token,
|
|
184
184
|
* missing template, copy failure).
|
|
185
185
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/commands/init.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/commands/init.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ForgeCart channel credentials.
|
|
2
2
|
#
|
|
3
|
-
# `forgecart init` writes the real values to `.env
|
|
3
|
+
# `forgecart init` writes the real values to `.env` for you. This file
|
|
4
4
|
# documents the variables the storefront reads. Both are required and used
|
|
5
5
|
# server-side only (never exposed to the browser).
|
|
6
6
|
|
|
@@ -17,8 +17,8 @@ The storefront reads two environment variables (server-side only):
|
|
|
17
17
|
| `FORGECART_CHANNEL_TOKEN` | Identifies the channel; sent as the `forgecart-token` header. |
|
|
18
18
|
| `FORGECART_SHOP_API_URL` | Base URL of the channel's shop GraphQL API (`.../shop-api`). |
|
|
19
19
|
|
|
20
|
-
`forgecart init` writes both to `.env
|
|
21
|
-
copy `.env.example` to `.env
|
|
20
|
+
`forgecart init` writes both to `.env` for you. To configure manually,
|
|
21
|
+
copy `.env.example` to `.env` and fill them in.
|
|
22
22
|
|
|
23
23
|
These values are consumed in [`src/lib/forgecart.ts`](src/lib/forgecart.ts),
|
|
24
24
|
which constructs and memoizes a singleton `ForgeCartShopClient`:
|
|
@@ -4,22 +4,23 @@ const nextConfig = {
|
|
|
4
4
|
// storefront with `next start` without a full node_modules tree.
|
|
5
5
|
output: 'standalone',
|
|
6
6
|
reactStrictMode: true,
|
|
7
|
-
// The visual-editor preview runs THIS dev server inside a workspace pod
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// .
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// `next start` (deployed storefronts) ignores it.
|
|
7
|
+
// The visual-editor preview runs THIS dev server inside a workspace pod,
|
|
8
|
+
// embedded cross-origin in the dashboard's editor iframe. Next 16 ENFORCES
|
|
9
|
+
// `allowedDevOrigins`: it 403s any cross-site request to an internal dev
|
|
10
|
+
// resource (`/_next`, `/__nextjs`, the HMR websocket) whose Origin/Referer
|
|
11
|
+
// host is not localhost/127.0.0.1 and not listed here — silently killing the
|
|
12
|
+
// dashboard iframe's dev channel with no page error. So this list must name
|
|
13
|
+
// every ORIGIN THAT EMBEDS the editor — i.e. each env's DASHBOARD host — not
|
|
14
|
+
// just the preview subdomain. GOTCHA: `*` matches exactly ONE label (Next's
|
|
15
|
+
// `matchWildcardDomain`), so `*.forgecart.dev` does NOT cover the two-label
|
|
16
|
+
// prod preview host `<code>.preview.forgecart.dev` — it must be listed
|
|
17
|
+
// explicitly. Dev-only — `next start` (deployed storefronts) ignores it.
|
|
18
18
|
allowedDevOrigins: [
|
|
19
|
-
'*.
|
|
20
|
-
'*.
|
|
21
|
-
'*.
|
|
22
|
-
'*.forgecart.dev',
|
|
19
|
+
'*.vm.forgecart.com', // `pnpm vm` Cloudflare-tunnel dashboards
|
|
20
|
+
'*.dev.forgecart.dev', // dev/test dashboard + preview subdomains
|
|
21
|
+
'*.forgecart.dev', // single-label deployed preview hosts
|
|
22
|
+
'*.preview.forgecart.dev', // two-label prod preview host (`*.forgecart.dev` can't match it)
|
|
23
|
+
'dashboard.forgecart.com', // prod dashboard origin that embeds the editor
|
|
23
24
|
],
|
|
24
25
|
experimental: {
|
|
25
26
|
// Persist Turbopack's dev compile artifacts to `.next` so a pod can serve
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
"start": "next start"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@forgecart/designer-runtime": "^1.
|
|
11
|
+
"@forgecart/designer-runtime": "^1.202607161748.0",
|
|
12
12
|
"@forgecart/sdk": "^1.2.6",
|
|
13
13
|
"next": "^16.1.0",
|
|
14
14
|
"react": "^19.0.0",
|
|
15
|
-
"react-dom": "^19.0.0"
|
|
15
|
+
"react-dom": "^19.0.0",
|
|
16
|
+
"uuidv7": "^1.1.0"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
19
|
"@types/node": "^22.10.0",
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import type { NextRequest } from 'next/server';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
forwardTrackEvent,
|
|
6
|
+
getUpstreamConfig,
|
|
7
|
+
isObviousBot,
|
|
8
|
+
type TrackEventInput,
|
|
9
|
+
} from '../../../lib/track-forward';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Same-origin marketing-event relay (design doc S7).
|
|
13
|
+
*
|
|
14
|
+
* `ForgeTracker` batches client events (page_view, cta_click, heartbeat, …)
|
|
15
|
+
* and POSTs them here; this handler forwards each item to the ForgeCart shop
|
|
16
|
+
* API as a `trackEvent` GraphQL mutation. Unlike `__forge_beacon` this route
|
|
17
|
+
* is a PRODUCTION feature — tracking runs on deployed storefronts — with one
|
|
18
|
+
* inert guard: before `forgecart init` writes `.env` (the image-build
|
|
19
|
+
* prewarm) there is no shop to talk to, so every item answers `accepted:false`
|
|
20
|
+
* without any upstream call and the server never crashes.
|
|
21
|
+
*
|
|
22
|
+
* The upstream hop itself (raw HTTP GraphQL POST with the channel token, the
|
|
23
|
+
* shopper session as Bearer, UA + `x-forwarded-for` enrichment, and the
|
|
24
|
+
* why-not-the-SDK rationale) lives in `src/lib/track-forward.ts` — shared with
|
|
25
|
+
* the F-B `experiment_exposure` emission so both server-side emitters speak
|
|
26
|
+
* one contract. This route owns everything batch-shaped: parsing, per-item
|
|
27
|
+
* validation, the bot gate, session threading, and cookie persistence.
|
|
28
|
+
*
|
|
29
|
+
* Session capture mirrors `cart-actions.ts#persistSession`: the shop API
|
|
30
|
+
* surfaces a freshly minted session in the response `extensions`
|
|
31
|
+
* (`forgecart-auth-token`) exactly once; the first touched item captures it,
|
|
32
|
+
* the remaining items of the batch ride it as Bearer (one identity per batch),
|
|
33
|
+
* and the relay response persists it into the httpOnly `forgecart-session`
|
|
34
|
+
* cookie with the same attributes the cart actions use — the first tracked
|
|
35
|
+
* interaction establishes the shopper identity the cart will later reuse.
|
|
36
|
+
*
|
|
37
|
+
* Contract: per-item isolation (one upstream failure never kills the batch),
|
|
38
|
+
* `accepted:false` is TERMINAL for that item (the tracker never retries), and
|
|
39
|
+
* obviously non-human user agents short-circuit the whole batch with zero
|
|
40
|
+
* upstream calls.
|
|
41
|
+
*
|
|
42
|
+
* Folder name: `%5F%5Ffc` is URL-encoded `__fc` — the App Router treats
|
|
43
|
+
* `_`-prefixed folders as PRIVATE (excluded from routing), and the `%5F`
|
|
44
|
+
* escape is Next's documented way to serve a literal-underscore URL segment.
|
|
45
|
+
* A folder literally named `__fc` would silently 404.
|
|
46
|
+
*/
|
|
47
|
+
export const dynamic = 'force-dynamic';
|
|
48
|
+
|
|
49
|
+
const SESSION_COOKIE = 'forgecart-session';
|
|
50
|
+
/** Mirrors `cart-actions.ts#persistSession` — the cookie is one shared identity. */
|
|
51
|
+
const SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
|
|
52
|
+
|
|
53
|
+
/** The tracker flushes at ≤10 events; anything past this is not our client. */
|
|
54
|
+
const MAX_BATCH_SIZE = 50;
|
|
55
|
+
|
|
56
|
+
/** Canonical textual UUID — the only client `eventId` shape the API accepts. */
|
|
57
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
58
|
+
|
|
59
|
+
interface TrackItemResult {
|
|
60
|
+
accepted: boolean;
|
|
61
|
+
eventId: string | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
65
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Parse the request body into a bounded batch array, or `null` when malformed. */
|
|
69
|
+
async function parseBatch(request: NextRequest): Promise<unknown[] | null> {
|
|
70
|
+
let body: unknown;
|
|
71
|
+
try {
|
|
72
|
+
body = await request.json();
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (!isPlainObject(body) || !Array.isArray(body.events)) return null;
|
|
77
|
+
if (body.events.length === 0 || body.events.length > MAX_BATCH_SIZE) return null;
|
|
78
|
+
return body.events;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Copy an optional string field from the raw item, rejecting non-string junk. */
|
|
82
|
+
function readOptionalString(value: unknown): string | undefined {
|
|
83
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate one raw batch item into a `TrackEventInput`, or `null` when the
|
|
88
|
+
* item is malformed (missing/empty `eventType`, non-UUID `eventId`, non-object
|
|
89
|
+
* `properties`). Only allowlisted fields cross to the upstream call.
|
|
90
|
+
*/
|
|
91
|
+
function validateItem(raw: unknown): TrackEventInput | null {
|
|
92
|
+
if (!isPlainObject(raw)) return null;
|
|
93
|
+
const { eventType, eventId, properties } = raw;
|
|
94
|
+
if (typeof eventType !== 'string' || eventType.length === 0) return null;
|
|
95
|
+
if (eventId !== undefined && (typeof eventId !== 'string' || !UUID_PATTERN.test(eventId))) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (properties !== undefined && !isPlainObject(properties)) return null;
|
|
99
|
+
|
|
100
|
+
const input: TrackEventInput = { eventType };
|
|
101
|
+
if (typeof eventId === 'string') input.eventId = eventId;
|
|
102
|
+
const occurredAt = readOptionalString(raw.occurredAt);
|
|
103
|
+
if (occurredAt) input.occurredAt = occurredAt;
|
|
104
|
+
if (isPlainObject(properties)) input.properties = properties;
|
|
105
|
+
const utmSource = readOptionalString(raw.utmSource);
|
|
106
|
+
if (utmSource) input.utmSource = utmSource;
|
|
107
|
+
const utmMedium = readOptionalString(raw.utmMedium);
|
|
108
|
+
if (utmMedium) input.utmMedium = utmMedium;
|
|
109
|
+
const utmCampaign = readOptionalString(raw.utmCampaign);
|
|
110
|
+
if (utmCampaign) input.utmCampaign = utmCampaign;
|
|
111
|
+
const utmTerm = readOptionalString(raw.utmTerm);
|
|
112
|
+
if (utmTerm) input.utmTerm = utmTerm;
|
|
113
|
+
const utmContent = readOptionalString(raw.utmContent);
|
|
114
|
+
if (utmContent) input.utmContent = utmContent;
|
|
115
|
+
const currencyCode = readOptionalString(raw.currencyCode);
|
|
116
|
+
if (currencyCode) input.currencyCode = currencyCode;
|
|
117
|
+
return input;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
121
|
+
const batch = await parseBatch(request);
|
|
122
|
+
if (!batch) {
|
|
123
|
+
return NextResponse.json({ error: 'malformed batch' }, { status: 400 });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const rejectAll = (): NextResponse =>
|
|
127
|
+
NextResponse.json({
|
|
128
|
+
results: batch.map((): TrackItemResult => ({ accepted: false, eventId: null })),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Inert before `forgecart init` writes `.env` (image-prewarm contract).
|
|
132
|
+
if (!getUpstreamConfig()) return rejectAll();
|
|
133
|
+
|
|
134
|
+
const userAgent = request.headers.get('user-agent') ?? '';
|
|
135
|
+
if (isObviousBot(userAgent)) return rejectAll();
|
|
136
|
+
|
|
137
|
+
const incomingSession = request.cookies.get(SESSION_COOKIE)?.value ?? null;
|
|
138
|
+
const forwardedFor = request.headers.get('x-forwarded-for');
|
|
139
|
+
|
|
140
|
+
let sessionToken = incomingSession;
|
|
141
|
+
const results: TrackItemResult[] = [];
|
|
142
|
+
// Sequential on purpose: the first touched item mints the session and every
|
|
143
|
+
// later item of the batch must ride it — parallel sends would mint one
|
|
144
|
+
// identity per item and shatter attribution.
|
|
145
|
+
for (const raw of batch) {
|
|
146
|
+
const input = validateItem(raw);
|
|
147
|
+
if (!input) {
|
|
148
|
+
results.push({ accepted: false, eventId: null });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const outcome = await forwardTrackEvent(input, { sessionToken, userAgent, forwardedFor });
|
|
152
|
+
results.push({ accepted: outcome.accepted, eventId: outcome.eventId });
|
|
153
|
+
if (outcome.sessionToken) sessionToken = outcome.sessionToken;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const response = NextResponse.json({ results });
|
|
157
|
+
// Persist a newly minted (or upstream-refreshed) session exactly like
|
|
158
|
+
// `cart-actions.ts#persistSession` — same cookie, same attributes.
|
|
159
|
+
if (sessionToken && sessionToken !== incomingSession) {
|
|
160
|
+
response.cookies.set(SESSION_COOKIE, sessionToken, {
|
|
161
|
+
httpOnly: true,
|
|
162
|
+
sameSite: 'lax',
|
|
163
|
+
path: '/',
|
|
164
|
+
maxAge: SESSION_COOKIE_MAX_AGE_SECONDS,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return response;
|
|
168
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
-
import { ForgeAnalytics } from '../components/ForgeAnalytics';
|
|
5
4
|
import { ForgeErrorBeacon } from '../components/ForgeErrorBeacon';
|
|
5
|
+
import { ForgeTracker } from '../components/ForgeTracker';
|
|
6
6
|
import { ForgecartDesigner } from '../components/ForgecartDesigner';
|
|
7
7
|
import { Header } from '../components/Header';
|
|
8
8
|
import { getCart } from '../lib/cart-actions';
|
|
@@ -31,7 +31,7 @@ export default async function RootLayout({ children }: { children: ReactNode })
|
|
|
31
31
|
</CartProvider>
|
|
32
32
|
<ForgecartDesigner />
|
|
33
33
|
<ForgeErrorBeacon />
|
|
34
|
-
<
|
|
34
|
+
<ForgeTracker
|
|
35
35
|
shopApiUrl={process.env.FORGECART_SHOP_API_URL ?? ''}
|
|
36
36
|
channelToken={process.env.FORGECART_CHANNEL_TOKEN ?? ''}
|
|
37
37
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Link from 'next/link';
|
|
2
2
|
|
|
3
3
|
import { ProductCard } from '../components/ProductCard';
|
|
4
|
+
import { getVariant } from '../lib/experiments';
|
|
4
5
|
import { getFeaturedProducts } from '../lib/forgecart';
|
|
5
6
|
|
|
6
7
|
// Product data changes in the dashboard, so render on each request.
|
|
@@ -8,16 +9,27 @@ export const dynamic = 'force-dynamic';
|
|
|
8
9
|
|
|
9
10
|
export default async function HomePage() {
|
|
10
11
|
const products = await getFeaturedProducts(4);
|
|
12
|
+
// A/B example (design doc F-B): variants are code branches keyed on
|
|
13
|
+
// `variantKey`. `hero-headline` is the template's reference experiment —
|
|
14
|
+
// create one with that key (variants `control` + `b`) in the dashboard and
|
|
15
|
+
// this hero starts splitting; until then `variantKey` is null and the
|
|
16
|
+
// default copy renders. `data-fc-variant` mirrors the assignment for
|
|
17
|
+
// debugging/verification; exposure tracking happens server-side.
|
|
18
|
+
const hero = await getVariant('hero-headline');
|
|
11
19
|
|
|
12
20
|
return (
|
|
13
21
|
<div className="space-y-12">
|
|
14
|
-
<section
|
|
22
|
+
<section
|
|
23
|
+
data-fc-variant={hero.variantKey ?? 'default'}
|
|
24
|
+
className="rounded-2xl bg-white p-10 shadow-sm"
|
|
25
|
+
>
|
|
15
26
|
<h1 className="text-3xl font-bold tracking-tight text-gray-900">
|
|
16
27
|
Welcome to the shop
|
|
17
28
|
</h1>
|
|
18
29
|
<p className="mt-3 max-w-2xl text-gray-600">
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
{hero.variantKey === 'b'
|
|
31
|
+
? 'Fresh picks, fair prices — see what everyone is adding to their carts.'
|
|
32
|
+
: 'This storefront is served by a ForgeCart channel. Browse the catalog and add products to your cart.'}
|
|
21
33
|
</p>
|
|
22
34
|
<Link
|
|
23
35
|
href="/products"
|
|
@@ -33,6 +33,7 @@ export function CartView({ channelGroups }: { channelGroups: SellingPlanGroup[]
|
|
|
33
33
|
<p className="text-gray-500">Your cart is empty.</p>
|
|
34
34
|
<Link
|
|
35
35
|
href="/products"
|
|
36
|
+
data-fc-track="cart-browse-products"
|
|
36
37
|
className="inline-block rounded-md bg-gray-900 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-700"
|
|
37
38
|
>
|
|
38
39
|
Browse products
|
|
@@ -134,6 +135,7 @@ export function CartView({ channelGroups }: { channelGroups: SellingPlanGroup[]
|
|
|
134
135
|
type="button"
|
|
135
136
|
onClick={() => setSellingPlan(plan.id)}
|
|
136
137
|
disabled={pending}
|
|
138
|
+
data-fc-track="cart-subscribe-order"
|
|
137
139
|
aria-pressed={active}
|
|
138
140
|
className={`rounded-md border px-3 py-2 text-left text-sm transition disabled:opacity-50 ${
|
|
139
141
|
active
|