@forgecart/cli 2.202610052143.0 → 2.202610060357.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.
Files changed (125) hide show
  1. package/package.json +1 -1
  2. package/templates/storefront-shadcn/.forgecartignore +2 -0
  3. package/templates/storefront-shadcn/Procfile +1 -0
  4. package/templates/storefront-shadcn/README.md +229 -0
  5. package/templates/storefront-shadcn/SEO-MIGRATION.md +708 -0
  6. package/templates/storefront-shadcn/components.json +21 -0
  7. package/templates/storefront-shadcn/next.config.js +105 -0
  8. package/templates/storefront-shadcn/package.json +39 -0
  9. package/templates/storefront-shadcn/postcss.config.js +5 -0
  10. package/templates/storefront-shadcn/src/app/%5F%5Ffc/identify/route.ts +205 -0
  11. package/templates/storefront-shadcn/src/app/%5F%5Ffc/track/route.ts +189 -0
  12. package/templates/storefront-shadcn/src/app/%5F%5Fforge_beacon/route.ts +87 -0
  13. package/templates/storefront-shadcn/src/app/api/%5F%5Fbackend/methods/route.ts +27 -0
  14. package/templates/storefront-shadcn/src/app/cart/page.tsx +53 -0
  15. package/templates/storefront-shadcn/src/app/checkout/page.tsx +53 -0
  16. package/templates/storefront-shadcn/src/app/error.tsx +23 -0
  17. package/templates/storefront-shadcn/src/app/global-error.tsx +23 -0
  18. package/templates/storefront-shadcn/src/app/globals.css +156 -0
  19. package/templates/storefront-shadcn/src/app/layout.tsx +185 -0
  20. package/templates/storefront-shadcn/src/app/page.tsx +217 -0
  21. package/templates/storefront-shadcn/src/app/pages/[slug]/not-found.tsx +24 -0
  22. package/templates/storefront-shadcn/src/app/pages/[slug]/page.tsx +115 -0
  23. package/templates/storefront-shadcn/src/app/ping/route.ts +21 -0
  24. package/templates/storefront-shadcn/src/app/products/[slug]/not-found.tsx +18 -0
  25. package/templates/storefront-shadcn/src/app/products/[slug]/page.tsx +317 -0
  26. package/templates/storefront-shadcn/src/app/products/page.tsx +107 -0
  27. package/templates/storefront-shadcn/src/app/register/page.tsx +54 -0
  28. package/templates/storefront-shadcn/src/app/reset-password/page.tsx +60 -0
  29. package/templates/storefront-shadcn/src/app/robots.ts +69 -0
  30. package/templates/storefront-shadcn/src/app/sitemap.ts +106 -0
  31. package/templates/storefront-shadcn/src/app/verify/page.tsx +157 -0
  32. package/templates/storefront-shadcn/src/components/CartView.tsx +333 -0
  33. package/templates/storefront-shadcn/src/components/ForgeErrorBeacon.tsx +102 -0
  34. package/templates/storefront-shadcn/src/components/ForgeTracker.tsx +479 -0
  35. package/templates/storefront-shadcn/src/components/ForgecartDesigner.tsx +43 -0
  36. package/templates/storefront-shadcn/src/components/Header.tsx +73 -0
  37. package/templates/storefront-shadcn/src/components/LanguageSwitcher.tsx +96 -0
  38. package/templates/storefront-shadcn/src/components/LocaleLink.tsx +49 -0
  39. package/templates/storefront-shadcn/src/components/ProductCard.tsx +59 -0
  40. package/templates/storefront-shadcn/src/components/ProductPurchase.tsx +235 -0
  41. package/templates/storefront-shadcn/src/components/account/AccountMessage.tsx +63 -0
  42. package/templates/storefront-shadcn/src/components/account/RegisterForm.tsx +257 -0
  43. package/templates/storefront-shadcn/src/components/account/RequestPasswordResetForm.tsx +93 -0
  44. package/templates/storefront-shadcn/src/components/account/ResetPasswordForm.tsx +163 -0
  45. package/templates/storefront-shadcn/src/components/checkout/AddressStep.tsx +271 -0
  46. package/templates/storefront-shadcn/src/components/checkout/CheckoutFlow.tsx +551 -0
  47. package/templates/storefront-shadcn/src/components/checkout/CheckoutGate.tsx +55 -0
  48. package/templates/storefront-shadcn/src/components/checkout/PaymentElementForm.tsx +140 -0
  49. package/templates/storefront-shadcn/src/components/checkout/PaymentFormEmbed.tsx +89 -0
  50. package/templates/storefront-shadcn/src/components/checkout/RatesStep.tsx +115 -0
  51. package/templates/storefront-shadcn/src/components/ui/alert.tsx +75 -0
  52. package/templates/storefront-shadcn/src/components/ui/badge.tsx +40 -0
  53. package/templates/storefront-shadcn/src/components/ui/button.tsx +64 -0
  54. package/templates/storefront-shadcn/src/components/ui/card.tsx +28 -0
  55. package/templates/storefront-shadcn/src/components/ui/input.tsx +26 -0
  56. package/templates/storefront-shadcn/src/components/ui/label.tsx +22 -0
  57. package/templates/storefront-shadcn/src/components/ui/native-select.tsx +27 -0
  58. package/templates/storefront-shadcn/src/components/ui/skeleton.tsx +21 -0
  59. package/templates/storefront-shadcn/src/components/ui/utils.ts +16 -0
  60. package/templates/storefront-shadcn/src/instrumentation.ts +109 -0
  61. package/templates/storefront-shadcn/src/lib/account/account-link.ts +76 -0
  62. package/templates/storefront-shadcn/src/lib/account/register-state.ts +133 -0
  63. package/templates/storefront-shadcn/src/lib/account/reset-password-state.ts +111 -0
  64. package/templates/storefront-shadcn/src/lib/account/verify-state.ts +56 -0
  65. package/templates/storefront-shadcn/src/lib/account-actions.ts +76 -0
  66. package/templates/storefront-shadcn/src/lib/account-session.ts +47 -0
  67. package/templates/storefront-shadcn/src/lib/action-result.ts +30 -0
  68. package/templates/storefront-shadcn/src/lib/asset-alt.ts +34 -0
  69. package/templates/storefront-shadcn/src/lib/backend-actions.ts +20 -0
  70. package/templates/storefront-shadcn/src/lib/backend-client.ts +47 -0
  71. package/templates/storefront-shadcn/src/lib/cart-context.tsx +236 -0
  72. package/templates/storefront-shadcn/src/lib/checkout-session.ts +185 -0
  73. package/templates/storefront-shadcn/src/lib/content/page-metadata.ts +113 -0
  74. package/templates/storefront-shadcn/src/lib/content/render-fields.tsx +256 -0
  75. package/templates/storefront-shadcn/src/lib/content/resolve-page.ts +143 -0
  76. package/templates/storefront-shadcn/src/lib/error-messages.ts +24 -0
  77. package/templates/storefront-shadcn/src/lib/experiments.ts +333 -0
  78. package/templates/storefront-shadcn/src/lib/forgecart.ts +464 -0
  79. package/templates/storefront-shadcn/src/lib/format.ts +89 -0
  80. package/templates/storefront-shadcn/src/lib/identify-forward.ts +152 -0
  81. package/templates/storefront-shadcn/src/lib/locale/channel-locales-loader.ts +169 -0
  82. package/templates/storefront-shadcn/src/lib/locale/channel-locales-map.ts +46 -0
  83. package/templates/storefront-shadcn/src/lib/locale/channel-locales.ts +191 -0
  84. package/templates/storefront-shadcn/src/lib/locale/grammar.ts +194 -0
  85. package/templates/storefront-shadcn/src/lib/locale/localized-path.ts +55 -0
  86. package/templates/storefront-shadcn/src/lib/locale/middleware-plan.ts +107 -0
  87. package/templates/storefront-shadcn/src/lib/locale/request-binding.ts +80 -0
  88. package/templates/storefront-shadcn/src/lib/locale/request-locale.ts +66 -0
  89. package/templates/storefront-shadcn/src/lib/marketing-params.ts +213 -0
  90. package/templates/storefront-shadcn/src/lib/money.ts +50 -0
  91. package/templates/storefront-shadcn/src/lib/seo/alternates.ts +123 -0
  92. package/templates/storefront-shadcn/src/lib/seo/json-ld.ts +266 -0
  93. package/templates/storefront-shadcn/src/lib/seo/metadata.ts +419 -0
  94. package/templates/storefront-shadcn/src/lib/seo/noindex.ts +218 -0
  95. package/templates/storefront-shadcn/src/lib/seo/public-origin.ts +166 -0
  96. package/templates/storefront-shadcn/src/lib/seo/redirect-plan.ts +86 -0
  97. package/templates/storefront-shadcn/src/lib/seo/resolve-path.ts +107 -0
  98. package/templates/storefront-shadcn/src/lib/seo/scaffolded-routes.ts +83 -0
  99. package/templates/storefront-shadcn/src/lib/seo/sidecar.ts +75 -0
  100. package/templates/storefront-shadcn/src/lib/seo/site-verification.ts +98 -0
  101. package/templates/storefront-shadcn/src/lib/seo/sitemap-cache.ts +114 -0
  102. package/templates/storefront-shadcn/src/lib/seo/sitemap-entries.ts +321 -0
  103. package/templates/storefront-shadcn/src/lib/session-actions.ts +61 -0
  104. package/templates/storefront-shadcn/src/lib/session-cookies.ts +98 -0
  105. package/templates/storefront-shadcn/src/lib/shop-config.ts +51 -0
  106. package/templates/storefront-shadcn/src/lib/shop-session.ts +151 -0
  107. package/templates/storefront-shadcn/src/lib/track-forward.ts +200 -0
  108. package/templates/storefront-shadcn/src/lib/uuid.ts +19 -0
  109. package/templates/storefront-shadcn/src/middleware.ts +379 -0
  110. package/templates/storefront-shadcn/src/seo/redirects.ts +44 -0
  111. package/templates/storefront-shadcn/src/server/app.module.ts +18 -0
  112. package/templates/storefront-shadcn/src/server/backend-api.ts +26 -0
  113. package/templates/storefront-shadcn/src/server/backend-method.decorator.ts +23 -0
  114. package/templates/storefront-shadcn/src/server/bootstrap.ts +122 -0
  115. package/templates/storefront-shadcn/src/server/customer-extras/customer-extras.module.ts +13 -0
  116. package/templates/storefront-shadcn/src/server/customer-extras/service/customer-extras.service.ts +58 -0
  117. package/templates/storefront-shadcn/src/server/customer-extras/type/customer-extras.types.ts +11 -0
  118. package/templates/storefront-shadcn/src/server/forge/live-revision.ts +158 -0
  119. package/templates/storefront-shadcn/src/server/forgecart/forgecart-client.factory.ts +69 -0
  120. package/templates/storefront-shadcn/src/server/forgecart/forgecart.module.ts +9 -0
  121. package/templates/storefront-shadcn/src/server/runner.ts +90 -0
  122. package/templates/storefront-shadcn/src/server/types.ts +36 -0
  123. package/templates/storefront-shadcn/tsconfig.json +25 -0
  124. package/templates/storefront-shadcn-sdk-floor.json +1174 -0
  125. package/templates/template-set.json +10 -0
@@ -0,0 +1,271 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+
5
+ import type { ExtractedError } from '@forgecart/sdk/shop';
6
+
7
+ import type { Country } from '../../lib/forgecart';
8
+ import { resolveAddressSuggestion, suggestAddresses } from '../../lib/checkout-session';
9
+ import { shopperMessage } from '../../lib/error-messages';
10
+ import { mintUuid } from '../../lib/uuid';
11
+ import { Alert, AlertDescription } from '../ui/alert';
12
+ import { Button } from '../ui/button';
13
+ import { Input } from '../ui/input';
14
+ import { Label } from '../ui/label';
15
+ import { NativeSelect } from '../ui/native-select';
16
+
17
+ /** The form's value shape — field names mirror `CreateAddressInput`. */
18
+ export interface AddressFormValue {
19
+ streetLine1: string;
20
+ streetLine2: string;
21
+ city: string;
22
+ province: string;
23
+ postalCode: string;
24
+ countryId: string;
25
+ phoneNumber: string;
26
+ }
27
+
28
+ export const EMPTY_ADDRESS: AddressFormValue = {
29
+ streetLine1: '',
30
+ streetLine2: '',
31
+ city: '',
32
+ province: '',
33
+ postalCode: '',
34
+ countryId: '',
35
+ phoneNumber: '',
36
+ };
37
+
38
+ const SUGGEST_DEBOUNCE_MS = 300;
39
+
40
+ /**
41
+ * Address form: type-ahead via the shop's `addressAutocomplete` (an optional
42
+ * capability — an empty suggestion list simply leaves manual entry, which is
43
+ * always present), a country select populated from `availableCountries` (the
44
+ * API, never a hardcoded list — channel countries are channel-specific), and
45
+ * the REQUIRED phone field (#863's server gate; the `fieldErrors` map routes
46
+ * `SHIPPING_ADDRESS_PHONE_REQUIRED` / `SHIPPING_ADDRESS_COUNTRY_*` here).
47
+ */
48
+ export function AddressStep({
49
+ countries,
50
+ value,
51
+ onChange,
52
+ onSubmit,
53
+ pending,
54
+ error,
55
+ }: {
56
+ countries: Country[];
57
+ value: AddressFormValue;
58
+ onChange: (value: AddressFormValue) => void;
59
+ onSubmit: () => void;
60
+ pending: boolean;
61
+ error: ExtractedError | null;
62
+ }) {
63
+ const [suggestions, setSuggestions] = useState<{ id: string; description: string }[]>([]);
64
+ const [suggestOpen, setSuggestOpen] = useState(false);
65
+ // One vendor billing session per address-entry (Google `sessiontoken`).
66
+ // mintUuid, not crypto.randomUUID: the latter is secure-context gated and
67
+ // absent on the plain-http local preview origin — a bare call crashed this
68
+ // whole step into the route error boundary (2026-08-09).
69
+ const sessionTokenRef = useRef<string>('');
70
+ if (!sessionTokenRef.current) sessionTokenRef.current = mintUuid();
71
+ const debounceRef = useRef<number | null>(null);
72
+
73
+ const selectedCountry = useMemo(
74
+ () => countries.find((country) => country.id === value.countryId),
75
+ [countries, value.countryId],
76
+ );
77
+
78
+ useEffect(() => {
79
+ return () => {
80
+ if (debounceRef.current !== null) window.clearTimeout(debounceRef.current);
81
+ };
82
+ }, []);
83
+
84
+ function onStreetInput(street: string): void {
85
+ onChange({ ...value, streetLine1: street });
86
+ if (debounceRef.current !== null) window.clearTimeout(debounceRef.current);
87
+ if (street.trim().length < 4) {
88
+ setSuggestions([]);
89
+ setSuggestOpen(false);
90
+ return;
91
+ }
92
+ debounceRef.current = window.setTimeout(async () => {
93
+ const result = await suggestAddresses(street, sessionTokenRef.current, selectedCountry?.code);
94
+ // Suggest failures degrade to manual entry — never an error surface.
95
+ if (result.ok && result.data.length > 0) {
96
+ setSuggestions(result.data);
97
+ setSuggestOpen(true);
98
+ return;
99
+ }
100
+ setSuggestions([]);
101
+ setSuggestOpen(false);
102
+ }, SUGGEST_DEBOUNCE_MS);
103
+ }
104
+
105
+ async function pickSuggestion(id: string): Promise<void> {
106
+ setSuggestOpen(false);
107
+ const result = await resolveAddressSuggestion(id, sessionTokenRef.current);
108
+ if (!result.ok) return;
109
+ const resolved = result.data;
110
+ const matchedCountry = countries.find((country) => country.code === resolved.countryCode);
111
+ onChange({
112
+ ...value,
113
+ streetLine1: resolved.streetLine1,
114
+ streetLine2: resolved.streetLine2 ?? '',
115
+ city: resolved.city ?? '',
116
+ province: resolved.province ?? '',
117
+ postalCode: resolved.postalCode ?? '',
118
+ // Suggestion outside the channel's shipping countries: keep the text
119
+ // fields, leave the select for the shopper (the server would reject an
120
+ // unknown code anyway).
121
+ countryId: matchedCountry?.id ?? value.countryId,
122
+ });
123
+ }
124
+
125
+ const phoneError = error?.code === 'SHIPPING_ADDRESS_PHONE_REQUIRED';
126
+ const countryError =
127
+ error?.code === 'SHIPPING_ADDRESS_COUNTRY_REQUIRED' ||
128
+ error?.code === 'SHIPPING_ADDRESS_COUNTRY_INVALID';
129
+ const stepError = error && !phoneError && !countryError;
130
+
131
+ return (
132
+ <form
133
+ className="space-y-4"
134
+ onSubmit={(e) => {
135
+ e.preventDefault();
136
+ onSubmit();
137
+ }}
138
+ >
139
+ <div className="relative grid gap-2">
140
+ <Label htmlFor="street-line-1">Street address</Label>
141
+ <Input
142
+ id="street-line-1"
143
+ type="text"
144
+ required
145
+ autoComplete="address-line1"
146
+ value={value.streetLine1}
147
+ onChange={(e) => onStreetInput(e.target.value)}
148
+ onFocus={() => suggestions.length > 0 && setSuggestOpen(true)}
149
+ />
150
+ {suggestOpen && suggestions.length > 0 && (
151
+ <ul className="absolute top-full z-10 mt-1 w-full overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
152
+ {suggestions.map((suggestion) => (
153
+ <li key={suggestion.id}>
154
+ <button
155
+ type="button"
156
+ onClick={() => void pickSuggestion(suggestion.id)}
157
+ className="w-full rounded-sm px-2 py-1.5 text-left text-sm outline-none hover:bg-muted focus-visible:bg-muted"
158
+ >
159
+ {suggestion.description}
160
+ </button>
161
+ </li>
162
+ ))}
163
+ </ul>
164
+ )}
165
+ </div>
166
+
167
+ <div className="grid gap-2">
168
+ <Label htmlFor="street-line-2">Apartment, suite, etc. (optional)</Label>
169
+ <Input
170
+ id="street-line-2"
171
+ type="text"
172
+ autoComplete="address-line2"
173
+ value={value.streetLine2}
174
+ onChange={(e) => onChange({ ...value, streetLine2: e.target.value })}
175
+ />
176
+ </div>
177
+
178
+ <div className="grid grid-cols-2 gap-4">
179
+ <div className="grid gap-2">
180
+ <Label htmlFor="city">City</Label>
181
+ <Input
182
+ id="city"
183
+ type="text"
184
+ required
185
+ autoComplete="address-level2"
186
+ value={value.city}
187
+ onChange={(e) => onChange({ ...value, city: e.target.value })}
188
+ />
189
+ </div>
190
+ <div className="grid gap-2">
191
+ <Label htmlFor="province">State / Province</Label>
192
+ <Input
193
+ id="province"
194
+ type="text"
195
+ autoComplete="address-level1"
196
+ value={value.province}
197
+ onChange={(e) => onChange({ ...value, province: e.target.value })}
198
+ />
199
+ </div>
200
+ </div>
201
+
202
+ <div className="grid grid-cols-2 gap-4">
203
+ <div className="grid gap-2">
204
+ <Label htmlFor="postal-code">Postal code</Label>
205
+ <Input
206
+ id="postal-code"
207
+ type="text"
208
+ required
209
+ autoComplete="postal-code"
210
+ value={value.postalCode}
211
+ onChange={(e) => onChange({ ...value, postalCode: e.target.value })}
212
+ />
213
+ </div>
214
+ <div className="grid gap-2">
215
+ <Label htmlFor="country">Country</Label>
216
+ <NativeSelect
217
+ id="country"
218
+ required
219
+ value={value.countryId}
220
+ onChange={(e) => onChange({ ...value, countryId: e.target.value })}
221
+ aria-invalid={countryError}
222
+ aria-describedby={countryError ? 'country-error' : undefined}
223
+ >
224
+ <option value="" disabled>
225
+ Select a country
226
+ </option>
227
+ {countries.map((country) => (
228
+ <option key={country.id} value={country.id}>
229
+ {country.name}
230
+ </option>
231
+ ))}
232
+ </NativeSelect>
233
+ {countryError && (
234
+ <p id="country-error" className="text-sm text-destructive">
235
+ {shopperMessage(error)}
236
+ </p>
237
+ )}
238
+ </div>
239
+ </div>
240
+
241
+ <div className="grid gap-2">
242
+ <Label htmlFor="phone-number">Phone number</Label>
243
+ <Input
244
+ id="phone-number"
245
+ type="tel"
246
+ required
247
+ autoComplete="tel"
248
+ value={value.phoneNumber}
249
+ onChange={(e) => onChange({ ...value, phoneNumber: e.target.value })}
250
+ aria-invalid={phoneError}
251
+ aria-describedby={phoneError ? 'phone-error' : undefined}
252
+ />
253
+ {phoneError && (
254
+ <p id="phone-error" className="text-sm text-destructive">
255
+ {shopperMessage(error)}
256
+ </p>
257
+ )}
258
+ </div>
259
+
260
+ {stepError && (
261
+ <Alert role="alert" variant="destructive">
262
+ <AlertDescription>{shopperMessage(error)}</AlertDescription>
263
+ </Alert>
264
+ )}
265
+
266
+ <Button type="submit" disabled={pending} data-fc-track="checkout-address">
267
+ {pending ? 'Saving…' : 'Continue to shipping'}
268
+ </Button>
269
+ </form>
270
+ );
271
+ }