@forgecart/cli 2.202610141948.0 → 2.202610150140.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/package.json +1 -1
- package/templates/storefront/README.md +52 -5
- package/templates/storefront/src/app/account/page.tsx +51 -0
- package/templates/storefront/src/app/layout.tsx +4 -5
- package/templates/storefront/src/app/verify/page.tsx +16 -4
- package/templates/storefront/src/components/Header.tsx +13 -1
- package/templates/storefront/src/components/account/AccountPanel.tsx +289 -0
- package/templates/storefront/src/components/account/RegisterForm.tsx +15 -66
- package/templates/storefront/src/components/account/ResetPasswordForm.tsx +2 -2
- package/templates/storefront/src/components/account/VerificationResend.tsx +96 -0
- package/templates/storefront/src/lib/account/account-state.ts +152 -0
- package/templates/storefront/src/lib/account-session.ts +83 -16
- package/templates/storefront/src/lib/error-messages.ts +102 -8
- package/templates/storefront/src/lib/seo/noindex.ts +5 -0
- package/templates/storefront/src/lib/seo/sitemap-entries.ts +2 -0
- package/templates/storefront/src/lib/shop-session.ts +71 -18
- package/templates/storefront-shadcn/README.md +52 -5
- package/templates/storefront-shadcn/src/app/account/page.tsx +51 -0
- package/templates/storefront-shadcn/src/app/layout.tsx +4 -5
- package/templates/storefront-shadcn/src/app/verify/page.tsx +16 -4
- package/templates/storefront-shadcn/src/components/Header.tsx +17 -1
- package/templates/storefront-shadcn/src/components/account/AccountPanel.tsx +290 -0
- package/templates/storefront-shadcn/src/components/account/RegisterForm.tsx +19 -61
- package/templates/storefront-shadcn/src/components/account/ResetPasswordForm.tsx +2 -2
- package/templates/storefront-shadcn/src/components/account/VerificationResend.tsx +92 -0
- package/templates/storefront-shadcn/src/lib/account/account-state.ts +152 -0
- package/templates/storefront-shadcn/src/lib/account-session.ts +83 -16
- package/templates/storefront-shadcn/src/lib/error-messages.ts +102 -8
- package/templates/storefront-shadcn/src/lib/seo/noindex.ts +5 -0
- package/templates/storefront-shadcn/src/lib/seo/sitemap-entries.ts +2 -0
- package/templates/storefront-shadcn/src/lib/shop-session.ts +71 -18
|
@@ -31,6 +31,10 @@ import { getPublicOrigin } from './public-origin';
|
|
|
31
31
|
* no content of its own, identical for every visitor, so there is nothing for a
|
|
32
32
|
* search result to be about. One entry covers every language: this set is
|
|
33
33
|
* matched against the locale-STRIPPED path.
|
|
34
|
+
*
|
|
35
|
+
* `/account` (#1093) is here for the same funnel reason as `/register`: the
|
|
36
|
+
* server renders a skeleton, identical for every visitor, and everything the
|
|
37
|
+
* page shows is one shopper's own — sign-in form or greeting.
|
|
34
38
|
*/
|
|
35
39
|
export const NOINDEX_PATHS: readonly string[] = [
|
|
36
40
|
'/cart',
|
|
@@ -38,6 +42,7 @@ export const NOINDEX_PATHS: readonly string[] = [
|
|
|
38
42
|
'/verify',
|
|
39
43
|
'/reset-password',
|
|
40
44
|
'/register',
|
|
45
|
+
'/account',
|
|
41
46
|
];
|
|
42
47
|
|
|
43
48
|
/**
|
|
@@ -49,6 +49,8 @@ export const STATIC_ROUTES: readonly StaticRoute[] = [
|
|
|
49
49
|
// The register page (#1471). Suppressed for the same reason `/cart` is — a
|
|
50
50
|
// form with no content of its own — and listed for this table's stated one.
|
|
51
51
|
{ path: '/register', indexable: false },
|
|
52
|
+
// The account page (#1093): sign-in form or greeting, one shopper's own.
|
|
53
|
+
{ path: '/account', indexable: false },
|
|
52
54
|
];
|
|
53
55
|
|
|
54
56
|
/**
|
|
@@ -29,6 +29,10 @@ import { syncShopSession } from './session-actions';
|
|
|
29
29
|
* it from result extensions on the first mutation) is synced back into
|
|
30
30
|
* BOTH cookies via the `syncShopSession` server action — the next SSR
|
|
31
31
|
* pass sees the cart this socket built.
|
|
32
|
+
* - A sign-in or sign-out changes WHO the session is, and that never rides
|
|
33
|
+
* the capture: `replaceShopSession(token)` disposes the socket, opens a
|
|
34
|
+
* fresh one on the new session (or none) and writes both cookies before
|
|
35
|
+
* it resolves (`lib/account-session.ts` is its caller).
|
|
32
36
|
*
|
|
33
37
|
* Every operation returns the same `ActionResult` envelope the old Server
|
|
34
38
|
* Actions used, so calling components keep their error contract: typed
|
|
@@ -96,6 +100,24 @@ export function isSessionReady(): boolean {
|
|
|
96
100
|
return config !== null && typeof window !== 'undefined';
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
/** A client on the channel coordinates, carrying `token` — or no session when null. */
|
|
104
|
+
function openClient(coordinates: ShopSessionConfig, token: string | null): ForgeCartShopClient {
|
|
105
|
+
const opened = new ForgeCartShopClient({
|
|
106
|
+
endpoint: coordinates.apiUrl,
|
|
107
|
+
channelToken: coordinates.channelToken,
|
|
108
|
+
// Fixed at handshake — the cart, checkout and every error message on
|
|
109
|
+
// this socket answer in the language the URL resolved to.
|
|
110
|
+
languageCode: coordinates.languageCode,
|
|
111
|
+
// The visitor mirror is minted by the middleware before any page can
|
|
112
|
+
// mount the provider, so it is present on every real navigation; when
|
|
113
|
+
// it is somehow absent the client simply sends no visitor header and
|
|
114
|
+
// visitor-subject goals stay unstamped (never guess — #1079).
|
|
115
|
+
visitorId: readMirrorCookie(VISITOR_MIRROR_COOKIE) ?? undefined,
|
|
116
|
+
});
|
|
117
|
+
if (token) opened.setAuthToken(token);
|
|
118
|
+
return opened;
|
|
119
|
+
}
|
|
120
|
+
|
|
99
121
|
function ensureClient(): ForgeCartShopClient {
|
|
100
122
|
if (!config) {
|
|
101
123
|
throw new Error(
|
|
@@ -103,32 +125,63 @@ function ensureClient(): ForgeCartShopClient {
|
|
|
103
125
|
);
|
|
104
126
|
}
|
|
105
127
|
if (!client) {
|
|
106
|
-
client = new ForgeCartShopClient({
|
|
107
|
-
endpoint: config.apiUrl,
|
|
108
|
-
channelToken: config.channelToken,
|
|
109
|
-
// Fixed at handshake — the cart, checkout and every error message on
|
|
110
|
-
// this socket answer in the language the URL resolved to.
|
|
111
|
-
languageCode: config.languageCode,
|
|
112
|
-
// The visitor mirror is minted by the middleware before any page can
|
|
113
|
-
// mount the provider, so it is present on every real navigation; when
|
|
114
|
-
// it is somehow absent the client simply sends no visitor header and
|
|
115
|
-
// visitor-subject goals stay unstamped (never guess — #1079).
|
|
116
|
-
visitorId: readMirrorCookie(VISITOR_MIRROR_COOKIE) ?? undefined,
|
|
117
|
-
});
|
|
118
128
|
const existing = readMirrorSession();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
syncedToken = existing;
|
|
122
|
-
}
|
|
129
|
+
client = openClient(config, existing);
|
|
130
|
+
if (existing) syncedToken = existing;
|
|
123
131
|
}
|
|
124
132
|
return client;
|
|
125
133
|
}
|
|
126
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Move this tab onto another session — the ONE path an identity change takes
|
|
137
|
+
* (#1093): a sign-in hands over the session it minted, a sign-out hands over
|
|
138
|
+
* `null`.
|
|
139
|
+
*
|
|
140
|
+
* The socket is REPLACED, never re-pointed. Its identity was fixed at its
|
|
141
|
+
* handshake, and a sign-in that left the live socket speaking for the session
|
|
142
|
+
* it opened with was refused UNAUTHORIZED on its very next operation — the
|
|
143
|
+
* greeting — and rendered as a failed sign-in (#1089). So the old client is
|
|
144
|
+
* disposed, which is terminal (nothing still in flight on it can complete
|
|
145
|
+
* under the wrong identity), and a fresh one carrying `token` takes its place;
|
|
146
|
+
* its first operation handshakes as the new session. The platform now rebinds
|
|
147
|
+
* a connection that signs in, and the SDK re-handshakes on `setAuthToken`; the
|
|
148
|
+
* replacement stays regardless, so which identity every later operation runs
|
|
149
|
+
* under is a fact of this module rather than of the platform and SDK versions
|
|
150
|
+
* a workspace happens to run.
|
|
151
|
+
*
|
|
152
|
+
* A sign-out also clears the token the SDK persists in the browser's storage.
|
|
153
|
+
* A fresh client restores that token when it is constructed, so without the
|
|
154
|
+
* clear the replacement would carry the very session it was meant to leave.
|
|
155
|
+
*
|
|
156
|
+
* Both cookies are written next, and AWAITED — unlike the capture in
|
|
157
|
+
* `runSessionOp` — so a caller that resolves after this has left the next
|
|
158
|
+
* server render and the next visit's socket on the same session as this tab.
|
|
159
|
+
* A failed write is absorbed exactly as the capture absorbs one: the socket
|
|
160
|
+
* is the authority for this tab, and a lost mirror costs the next visit its
|
|
161
|
+
* session, never this interaction.
|
|
162
|
+
*
|
|
163
|
+
* Before the cart provider has armed the singleton there is no socket to
|
|
164
|
+
* replace, and only the cookies move.
|
|
165
|
+
*/
|
|
166
|
+
export async function replaceShopSession(token: string | null): Promise<void> {
|
|
167
|
+
client?.dispose();
|
|
168
|
+
client = config ? openClient(config, token) : null;
|
|
169
|
+
if (token === null) client?.clearAuthToken();
|
|
170
|
+
syncedToken = token;
|
|
171
|
+
try {
|
|
172
|
+
await syncShopSession(token);
|
|
173
|
+
} catch {
|
|
174
|
+
// Absorbed by design — see above: the socket already carries `token`.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
127
178
|
/**
|
|
128
179
|
* Run one session operation on the shopper's socket and return the typed
|
|
129
180
|
* envelope. Session capture: when the operation minted or rotated the
|
|
130
181
|
* session, the new token is synced into the cookies fire-and-forget —
|
|
131
|
-
* a sync failure never breaks the shopper's interaction.
|
|
182
|
+
* a sync failure never breaks the shopper's interaction. An operation whose
|
|
183
|
+
* socket was replaced while it ran captures nothing: its session is no longer
|
|
184
|
+
* this tab's, and writing it would undo the replacement.
|
|
132
185
|
*/
|
|
133
186
|
export async function runSessionOp<T>(
|
|
134
187
|
op: (client: ForgeCartShopClient) => Promise<T>,
|
|
@@ -140,7 +193,7 @@ export async function runSessionOp<T>(
|
|
|
140
193
|
try {
|
|
141
194
|
const data = await op(live);
|
|
142
195
|
const token = live.getAuthToken();
|
|
143
|
-
if (token && token !== syncedToken) {
|
|
196
|
+
if (token && token !== syncedToken && live === client) {
|
|
144
197
|
syncedToken = token;
|
|
145
198
|
void syncShopSession(token).catch(() => undefined);
|
|
146
199
|
}
|
|
@@ -155,10 +155,49 @@ defers its first flush so a fast add-to-cart wins the first-touch race. The
|
|
|
155
155
|
mirror is what the browser's shop socket boots its auth from: written without
|
|
156
156
|
it, a relay-established session would never reach the client at all.
|
|
157
157
|
|
|
158
|
+
## Customer account
|
|
159
|
+
|
|
160
|
+
The scaffold ships the shopper's account surface, so a store starts with
|
|
161
|
+
sign-in, sign-out and registration working — extend it rather than rebuild it:
|
|
162
|
+
|
|
163
|
+
| Route | What it does |
|
|
164
|
+
| ----------- | ------------------------------------------------------------ |
|
|
165
|
+
| `/account` | signs a shopper in, greets them by name, signs them out |
|
|
166
|
+
| `/register` | creates an account; its confirmation mail links to `/verify` |
|
|
167
|
+
|
|
168
|
+
The header links `/account`, the footer `/register`. Five rules keep it
|
|
169
|
+
working — each one was learned from a store that broke it:
|
|
170
|
+
|
|
171
|
+
- **Account pages are static shells.**
|
|
172
|
+
[`src/app/account/page.tsx`](src/app/account/page.tsx) reads no session on
|
|
173
|
+
the server — no `next/headers`, no `connection()`, no session module. The
|
|
174
|
+
session lives on the shopper's own socket, in the `AccountPanel` client
|
|
175
|
+
island, which waits for the `CartProvider` to arm it (`useCart().hydrated`).
|
|
176
|
+
A session read on the server path throws during the render and 500s the page.
|
|
177
|
+
- **Sign in and out only through the session helpers.** `loginCustomer` and
|
|
178
|
+
`logoutCustomer` ([`src/lib/account-session.ts`](src/lib/account-session.ts))
|
|
179
|
+
replace the socket with one carrying the new session (or none) and write both
|
|
180
|
+
session cookies before they resolve: a socket left on the session it opened
|
|
181
|
+
with refuses the very next call. `findSignedInCustomer` says who is signed in.
|
|
182
|
+
- **Every refusal renders through `shopperMessage`**
|
|
183
|
+
([`src/lib/error-messages.ts`](src/lib/error-messages.ts)): it has copy for
|
|
184
|
+
every account refusal the shop API answers, and appends the raw code to its
|
|
185
|
+
generic line for any code it does not know. A new refusal's copy goes there,
|
|
186
|
+
once.
|
|
187
|
+
- **Registration requires the password.** The API treats it as optional, and an
|
|
188
|
+
account registered without one can never sign in.
|
|
189
|
+
- **`lib/shop-config` is server-only**, so no client component imports it, and
|
|
190
|
+
**the header links only routes the template ships**.
|
|
191
|
+
|
|
192
|
+
A store scaffolded before the surface shipped adopts it with `forgecart
|
|
193
|
+
refresh`: the account files it never had are rendered, the files it never
|
|
194
|
+
edited are replaced, and a file it edited — its `Header.tsx`, say — is reported
|
|
195
|
+
as a conflict and left as it is.
|
|
196
|
+
|
|
158
197
|
## Account links
|
|
159
198
|
|
|
160
199
|
Two routes exist because the PLATFORM links to them from the e-mails it sends
|
|
161
|
-
a shopper
|
|
200
|
+
a shopper:
|
|
162
201
|
|
|
163
202
|
| Route | Mail | What it does |
|
|
164
203
|
| ---------------------------- | ------------------- | ------------------------------------------------------------ |
|
|
@@ -194,11 +233,14 @@ src/
|
|
|
194
233
|
cart-actions.ts # cart Server Actions (incl. coupons)
|
|
195
234
|
checkout-actions.ts # checkout Server Actions
|
|
196
235
|
cart-context.tsx # client cart state + typed error channel
|
|
197
|
-
|
|
198
|
-
account
|
|
236
|
+
shop-session.ts # the shopper's own socket + session cookie sync
|
|
237
|
+
account-session.ts # register, sign-in, sign-out on that socket
|
|
238
|
+
account-actions.ts # password-reset + verification-resend Server Actions
|
|
239
|
+
account/ # pure account rules (token, states, ErrorCode map)
|
|
240
|
+
error-messages.ts # shopper copy for every typed error code
|
|
199
241
|
components/
|
|
200
242
|
ui/ # the vendored shadcn/ui components + their cn helper
|
|
201
|
-
Header.tsx # top nav
|
|
243
|
+
Header.tsx # top nav (Home / Products / Cart / Account) + cart count
|
|
202
244
|
ProductCard.tsx # product tile
|
|
203
245
|
ProductPurchase.tsx # variant picker + purchase options + add-to-cart
|
|
204
246
|
CartView.tsx # cart lines, coupons, totals, checkout CTA
|
|
@@ -208,7 +250,10 @@ src/
|
|
|
208
250
|
RatesStep.tsx # grouped radio-card rate selection
|
|
209
251
|
PaymentFormEmbed.tsx # origin-checked hosted-form host
|
|
210
252
|
account/
|
|
211
|
-
|
|
253
|
+
AccountPanel.tsx # sign-in form, greeting, sign-out (/account)
|
|
254
|
+
RegisterForm.tsx # create an account (/register)
|
|
255
|
+
VerificationResend.tsx # send the confirmation mail again
|
|
256
|
+
AccountMessage.tsx # the one outcome block the account pages use
|
|
212
257
|
ResetPasswordForm.tsx # set a new password from a reset token
|
|
213
258
|
RequestPasswordResetForm.tsx # ask for a reset link
|
|
214
259
|
app/
|
|
@@ -220,6 +265,8 @@ src/
|
|
|
220
265
|
products/ # grid + [slug] detail
|
|
221
266
|
cart/page.tsx # cart
|
|
222
267
|
checkout/page.tsx # the stepped checkout
|
|
268
|
+
account/page.tsx # sign-in + greeting: a static shell around AccountPanel
|
|
269
|
+
register/page.tsx # create an account
|
|
223
270
|
verify/page.tsx # e-mail verification link target
|
|
224
271
|
reset-password/page.tsx # password-reset link target + request form
|
|
225
272
|
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
|
|
3
|
+
import { AccountPanel } from '../../components/account/AccountPanel';
|
|
4
|
+
import { getRequestLocale } from '../../lib/locale/request-binding';
|
|
5
|
+
import { routeMetadata } from '../../lib/seo/metadata';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `/account` — where a shopper signs in, is greeted, and signs out (#1093).
|
|
9
|
+
*
|
|
10
|
+
* A STATIC SHELL, and that is the law this file exists to keep: the server
|
|
11
|
+
* reads no session here — no cookie, no `connection()`, no session module. A
|
|
12
|
+
* storefront agent once read the session on the server path; the render threw
|
|
13
|
+
* `EmptyError: no elements in sequence`, and the 500 made the promote gate roll
|
|
14
|
+
* back the whole changeset. The session is the shopper's, on their own socket:
|
|
15
|
+
* `AccountPanel` asks it in the browser, and the server paints the skeleton it
|
|
16
|
+
* starts from. `tool/storefront-template-spec` holds this file to that.
|
|
17
|
+
*
|
|
18
|
+
* Locale-prefixable like `/register` — a shopper addresses it from a link on
|
|
19
|
+
* this storefront, never out of a mail — so `account` is absent from the
|
|
20
|
+
* grammar's unprefixed set and `/de/account` serves the German render.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const PAGE_TITLE = 'Your account';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Noindex via `NOINDEX_PATHS`, and no alternates — for the reasons `/register`'s
|
|
27
|
+
* metadata states: one shopper's own page has nothing for a search result to be
|
|
28
|
+
* about, and a page that declines indexing has no preferred address to claim.
|
|
29
|
+
*/
|
|
30
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
31
|
+
const { binding, shopName, channelResolved } = await getRequestLocale();
|
|
32
|
+
return routeMetadata({
|
|
33
|
+
binding,
|
|
34
|
+
shopName,
|
|
35
|
+
channelResolved,
|
|
36
|
+
pathname: '/account',
|
|
37
|
+
pathsByLocale: {},
|
|
38
|
+
title: PAGE_TITLE,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default async function AccountPage() {
|
|
43
|
+
const { binding } = await getRequestLocale();
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="mx-auto max-w-xl space-y-6">
|
|
47
|
+
<h1 className="text-2xl font-bold tracking-tight">{PAGE_TITLE}</h1>
|
|
48
|
+
<AccountPanel locale={binding} />
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -149,11 +149,10 @@ export default async function RootLayout({ children }: { children: ReactNode })
|
|
|
149
149
|
>
|
|
150
150
|
Cart
|
|
151
151
|
</LocaleLink>
|
|
152
|
-
{/* The
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
exist. A merchant who adds sign-in moves it up. */}
|
|
152
|
+
{/* The direct way to `/register` (#1471). The header's Account
|
|
153
|
+
link (#1093) is the account entry — sign-in, with
|
|
154
|
+
registration one link away — and this is the shortcut for
|
|
155
|
+
a shopper who already knows they are new. */}
|
|
157
156
|
<LocaleLink
|
|
158
157
|
href="/register"
|
|
159
158
|
locale={binding}
|
|
@@ -70,39 +70,51 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
70
70
|
* first thing a rebrand rewrites, while the state set is the contract.
|
|
71
71
|
*/
|
|
72
72
|
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* What each outcome tells the shopper, and where it sends them next. A verified
|
|
75
|
+
* account's next step is signing in (#1093), so that state links the account
|
|
76
|
+
* page; every other state sends the shopper back to the store.
|
|
77
|
+
*/
|
|
74
78
|
const VERIFY_COPY: Readonly<
|
|
75
|
-
Record<
|
|
79
|
+
Record<
|
|
80
|
+
VerifyState,
|
|
81
|
+
{ tone: AccountMessageTone; title: string; body: string; cta: string; href: string }
|
|
82
|
+
>
|
|
76
83
|
> = {
|
|
77
84
|
verified: {
|
|
78
85
|
tone: 'success',
|
|
79
86
|
title: 'Your e-mail address is verified',
|
|
80
87
|
body: 'Your account is ready — you can sign in with the address you registered.',
|
|
81
|
-
cta: '
|
|
88
|
+
cta: 'Sign in',
|
|
89
|
+
href: '/account',
|
|
82
90
|
},
|
|
83
91
|
'no-token': {
|
|
84
92
|
tone: 'warning',
|
|
85
93
|
title: 'This link is missing its verification token',
|
|
86
94
|
body: 'Open the link straight from the e-mail we sent you. It carries a one-time token this page needs, and copying only part of the address leaves it behind.',
|
|
87
95
|
cta: 'Go to the store',
|
|
96
|
+
href: '/',
|
|
88
97
|
},
|
|
89
98
|
'link-invalid': {
|
|
90
99
|
tone: 'warning',
|
|
91
100
|
title: 'This verification link is no longer valid',
|
|
92
101
|
body: 'It has either been used already or it has expired. If your account is still unverified, request a new verification e-mail and open the newest link.',
|
|
93
102
|
cta: 'Go to the store',
|
|
103
|
+
href: '/',
|
|
94
104
|
},
|
|
95
105
|
'wrong-store': {
|
|
96
106
|
tone: 'warning',
|
|
97
107
|
title: 'This link belongs to a different store',
|
|
98
108
|
body: 'The account it verifies does not shop here. Open the link from the e-mail that store sent you — every store sends its own.',
|
|
99
109
|
cta: 'Go to the store',
|
|
110
|
+
href: '/',
|
|
100
111
|
},
|
|
101
112
|
failed: {
|
|
102
113
|
tone: 'error',
|
|
103
114
|
title: 'We could not verify your e-mail address',
|
|
104
115
|
body: 'Something went wrong on our side. Please open the link again in a few minutes — and if it has stopped working by then, request a new verification e-mail.',
|
|
105
116
|
cta: 'Go to the store',
|
|
117
|
+
href: '/',
|
|
106
118
|
},
|
|
107
119
|
};
|
|
108
120
|
|
|
@@ -140,7 +152,7 @@ async function VerifyOutcome({ searchParams }: { searchParams: Promise<RouteSear
|
|
|
140
152
|
<AccountMessage tone={copy.tone} title={copy.title}>
|
|
141
153
|
{copy.body}
|
|
142
154
|
</AccountMessage>
|
|
143
|
-
<LocaleLink href=
|
|
155
|
+
<LocaleLink href={copy.href} locale={binding} className={buttonVariants()}>
|
|
144
156
|
{copy.cta}
|
|
145
157
|
</LocaleLink>
|
|
146
158
|
</div>
|
|
@@ -21,7 +21,16 @@ export interface HeaderProps {
|
|
|
21
21
|
languageCodes: readonly string[];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Top navigation: Home / Products / Cart / Account, with a live cart item count.
|
|
26
|
+
*
|
|
27
|
+
* Every link names a route this template SHIPS (#1093): a link to a page the
|
|
28
|
+
* scaffold does not carry is a 404 for the shopper and a false lead for the
|
|
29
|
+
* agent extending the store, and `tool/storefront-template-spec` holds this
|
|
30
|
+
* file to it. The Account link is static — it never renders who is signed in —
|
|
31
|
+
* so nothing here depends on the session and no promote can trip a hydration
|
|
32
|
+
* mismatch on it; the account page itself says who is signed in.
|
|
33
|
+
*/
|
|
25
34
|
export function Header({ locale, languageCodes }: HeaderProps) {
|
|
26
35
|
const { itemCount } = useCart();
|
|
27
36
|
|
|
@@ -65,6 +74,13 @@ export function Header({ locale, languageCodes }: HeaderProps) {
|
|
|
65
74
|
Cart
|
|
66
75
|
{mounted && itemCount > 0 && <Badge className="font-semibold">{itemCount}</Badge>}
|
|
67
76
|
</LocaleLink>
|
|
77
|
+
<LocaleLink
|
|
78
|
+
href="/account"
|
|
79
|
+
locale={locale}
|
|
80
|
+
className={buttonVariants({ variant: 'ghost', size: 'sm' })}
|
|
81
|
+
>
|
|
82
|
+
Account
|
|
83
|
+
</LocaleLink>
|
|
68
84
|
<LanguageSwitcher locale={locale} languageCodes={languageCodes} />
|
|
69
85
|
</nav>
|
|
70
86
|
</div>
|