@vendure-platform/create 0.5.1 → 1.0.0-major.202609091053.0.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 (172) hide show
  1. package/README.md +48 -36
  2. package/dist/add-to-existing.d.ts +19 -13
  3. package/dist/add-to-existing.js +1 -1
  4. package/dist/assets/demo-data/assets/branding/logo.svg +4 -0
  5. package/dist/assets/demo-data/assets/images/absorbent-pad.webp +0 -0
  6. package/dist/assets/demo-data/assets/images/adjustable-wrench.webp +0 -0
  7. package/dist/assets/demo-data/assets/images/anchor-bolt.webp +0 -0
  8. package/dist/assets/demo-data/assets/images/angle-grinder.webp +0 -0
  9. package/dist/assets/demo-data/assets/images/barrier-tape.webp +0 -0
  10. package/dist/assets/demo-data/assets/images/bench-grinder.webp +0 -0
  11. package/dist/assets/demo-data/assets/images/bracket.webp +0 -0
  12. package/dist/assets/demo-data/assets/images/broom.webp +0 -0
  13. package/dist/assets/demo-data/assets/images/bucket.webp +0 -0
  14. package/dist/assets/demo-data/assets/images/cable-gland.webp +0 -0
  15. package/dist/assets/demo-data/assets/images/cable-tie.webp +0 -0
  16. package/dist/assets/demo-data/assets/images/cable-tray.webp +0 -0
  17. package/dist/assets/demo-data/assets/images/cable.webp +0 -0
  18. package/dist/assets/demo-data/assets/images/chimney-base-kit.webp +0 -0
  19. package/dist/assets/demo-data/assets/images/chimney-cowl-bird-guard.webp +0 -0
  20. package/dist/assets/demo-data/assets/images/chimney-cowl-open.webp +0 -0
  21. package/dist/assets/demo-data/assets/images/chimney-storm-collar.webp +0 -0
  22. package/dist/assets/demo-data/assets/images/chimney-wall-bracket.webp +0 -0
  23. package/dist/assets/demo-data/assets/images/circular-saw.webp +0 -0
  24. package/dist/assets/demo-data/assets/images/cleaning-spray.webp +0 -0
  25. package/dist/assets/demo-data/assets/images/conduit.webp +0 -0
  26. package/dist/assets/demo-data/assets/images/cordless-drill.webp +0 -0
  27. package/dist/assets/demo-data/assets/images/drill-bits.webp +0 -0
  28. package/dist/assets/demo-data/assets/images/dust-mask.webp +0 -0
  29. package/dist/assets/demo-data/assets/images/ear-defenders.webp +0 -0
  30. package/dist/assets/demo-data/assets/images/ear-plugs.webp +0 -0
  31. package/dist/assets/demo-data/assets/images/extension-lead.webp +0 -0
  32. package/dist/assets/demo-data/assets/images/eye-wash-station.webp +0 -0
  33. package/dist/assets/demo-data/assets/images/face-shield.webp +0 -0
  34. package/dist/assets/demo-data/assets/images/file-set.webp +0 -0
  35. package/dist/assets/demo-data/assets/images/first-aid-kit.webp +0 -0
  36. package/dist/assets/demo-data/assets/images/floor-sign.webp +0 -0
  37. package/dist/assets/demo-data/assets/images/flue-bend-45.webp +0 -0
  38. package/dist/assets/demo-data/assets/images/flue-bend-90.webp +0 -0
  39. package/dist/assets/demo-data/assets/images/flue-section-1000mm.webp +0 -0
  40. package/dist/assets/demo-data/assets/images/flue-section-250mm.webp +0 -0
  41. package/dist/assets/demo-data/assets/images/flue-section-500mm.webp +0 -0
  42. package/dist/assets/demo-data/assets/images/fuse.webp +0 -0
  43. package/dist/assets/demo-data/assets/images/glove-dispenser.webp +0 -0
  44. package/dist/assets/demo-data/assets/images/grinding-disc.webp +0 -0
  45. package/dist/assets/demo-data/assets/images/hacksaw.webp +0 -0
  46. package/dist/assets/demo-data/assets/images/hammer.webp +0 -0
  47. package/dist/assets/demo-data/assets/images/hand-sanitiser.webp +0 -0
  48. package/dist/assets/demo-data/assets/images/head-torch.webp +0 -0
  49. package/dist/assets/demo-data/assets/images/heat-gun.webp +0 -0
  50. package/dist/assets/demo-data/assets/images/hex-bolt.webp +0 -0
  51. package/dist/assets/demo-data/assets/images/hex-key-set.webp +0 -0
  52. package/dist/assets/demo-data/assets/images/hex-nut.webp +0 -0
  53. package/dist/assets/demo-data/assets/images/hi-vis-jacket.webp +0 -0
  54. package/dist/assets/demo-data/assets/images/hi-vis-vest.webp +0 -0
  55. package/dist/assets/demo-data/assets/images/hose-clamp.webp +0 -0
  56. package/dist/assets/demo-data/assets/images/impact-driver.webp +0 -0
  57. package/dist/assets/demo-data/assets/images/jigsaw.webp +0 -0
  58. package/dist/assets/demo-data/assets/images/junction-box.webp +0 -0
  59. package/dist/assets/demo-data/assets/images/knee-pads.webp +0 -0
  60. package/dist/assets/demo-data/assets/images/machine-screw.webp +0 -0
  61. package/dist/assets/demo-data/assets/images/made-to-order-configurable-chimney.webp +0 -0
  62. package/dist/assets/demo-data/assets/images/mallet.webp +0 -0
  63. package/dist/assets/demo-data/assets/images/mcb.webp +0 -0
  64. package/dist/assets/demo-data/assets/images/mop.webp +0 -0
  65. package/dist/assets/demo-data/assets/images/paper-towels.webp +0 -0
  66. package/dist/assets/demo-data/assets/images/pipe-wrench.webp +0 -0
  67. package/dist/assets/demo-data/assets/images/pliers-set.webp +0 -0
  68. package/dist/assets/demo-data/assets/images/pry-bar.webp +0 -0
  69. package/dist/assets/demo-data/assets/images/rcd.webp +0 -0
  70. package/dist/assets/demo-data/assets/images/reciprocating-saw.webp +0 -0
  71. package/dist/assets/demo-data/assets/images/rivet.webp +0 -0
  72. package/dist/assets/demo-data/assets/images/roof-flashing-high-pitch.webp +0 -0
  73. package/dist/assets/demo-data/assets/images/roof-flashing-low-pitch.webp +0 -0
  74. package/dist/assets/demo-data/assets/images/safety-boots.webp +0 -0
  75. package/dist/assets/demo-data/assets/images/safety-glasses.webp +0 -0
  76. package/dist/assets/demo-data/assets/images/safety-harness.webp +0 -0
  77. package/dist/assets/demo-data/assets/images/safety-helmet.webp +0 -0
  78. package/dist/assets/demo-data/assets/images/safety-sign.webp +0 -0
  79. package/dist/assets/demo-data/assets/images/sanding-disc.webp +0 -0
  80. package/dist/assets/demo-data/assets/images/saw-blade.webp +0 -0
  81. package/dist/assets/demo-data/assets/images/screwdriver-set.webp +0 -0
  82. package/dist/assets/demo-data/assets/images/screwdriver.webp +0 -0
  83. package/dist/assets/demo-data/assets/images/self-tapping-screw.webp +0 -0
  84. package/dist/assets/demo-data/assets/images/socket-set.webp +0 -0
  85. package/dist/assets/demo-data/assets/images/socket.webp +0 -0
  86. package/dist/assets/demo-data/assets/images/spanner-set.webp +0 -0
  87. package/dist/assets/demo-data/assets/images/spanner.webp +0 -0
  88. package/dist/assets/demo-data/assets/images/spill-kit.webp +0 -0
  89. package/dist/assets/demo-data/assets/images/spirit-level.webp +0 -0
  90. package/dist/assets/demo-data/assets/images/switch.webp +0 -0
  91. package/dist/assets/demo-data/assets/images/tape-measure.webp +0 -0
  92. package/dist/assets/demo-data/assets/images/terminal-block.webp +0 -0
  93. package/dist/assets/demo-data/assets/images/threaded-rod.webp +0 -0
  94. package/dist/assets/demo-data/assets/images/torque-wrench.webp +0 -0
  95. package/dist/assets/demo-data/assets/images/utility-knife.webp +0 -0
  96. package/dist/assets/demo-data/assets/images/wall-pass-sleeve.webp +0 -0
  97. package/dist/assets/demo-data/assets/images/wall-plug.webp +0 -0
  98. package/dist/assets/demo-data/assets/images/washer.webp +0 -0
  99. package/dist/assets/demo-data/assets/images/waste-bin.webp +0 -0
  100. package/dist/assets/demo-data/assets/images/wire-connector.webp +0 -0
  101. package/dist/assets/demo-data/assets/images/wire-cutters.webp +0 -0
  102. package/dist/assets/demo-data/assets/images/wood-screw.webp +0 -0
  103. package/dist/assets/demo-data/assets/images/work-gloves.webp +0 -0
  104. package/dist/assets/demo-data/assets/images/work-light.webp +0 -0
  105. package/dist/assets/demo-data/assets/initial-data.json +667 -0
  106. package/dist/assets/demo-data/assets/products.csv +779 -0
  107. package/dist/assets/storefront/next-env.d.ts +2 -1
  108. package/dist/assets/storefront/next.config.ts +14 -13
  109. package/dist/assets/storefront/package.json +3 -2
  110. package/dist/assets/storefront/src/app/[locale]/account/addresses/page.tsx +2 -0
  111. package/dist/assets/storefront/src/app/[locale]/account/approvals/page.tsx +2 -0
  112. package/dist/assets/storefront/src/app/[locale]/account/company/business-units/page.tsx +2 -0
  113. package/dist/assets/storefront/src/app/[locale]/account/company/page.tsx +2 -0
  114. package/dist/assets/storefront/src/app/[locale]/account/company/payment-methods/page.tsx +2 -0
  115. package/dist/assets/storefront/src/app/[locale]/account/company/roles/page.tsx +2 -0
  116. package/dist/assets/storefront/src/app/[locale]/account/company/users/page.tsx +2 -0
  117. package/dist/assets/storefront/src/app/[locale]/account/profile/page.tsx +2 -0
  118. package/dist/assets/storefront/src/app/[locale]/account/quick-order/page.tsx +2 -0
  119. package/dist/assets/storefront/src/app/[locale]/account/quotes/[id]/page.tsx +10 -6
  120. package/dist/assets/storefront/src/app/[locale]/cart/order-summary.tsx +8 -5
  121. package/dist/assets/storefront/src/app/[locale]/cart/promotion-code.tsx +9 -6
  122. package/dist/assets/storefront/src/app/[locale]/checkout/page.tsx +2 -0
  123. package/dist/assets/storefront/src/app/[locale]/layout.tsx +3 -3
  124. package/dist/assets/storefront/src/app/[locale]/order-confirmation/[code]/order-confirmation.tsx +2 -60
  125. package/dist/assets/storefront/src/components/commerce/product-card-list.tsx +4 -2
  126. package/dist/assets/storefront/src/components/commerce/product-card.tsx +4 -2
  127. package/dist/assets/storefront/src/components/commerce/quote-action-buttons.tsx +31 -14
  128. package/dist/assets/storefront/src/config/capabilities.ts +41 -0
  129. package/dist/assets/storefront/src/config/scenario.ts +10 -0
  130. package/dist/assets/storefront/src/graphql-env.d.ts +47 -41
  131. package/dist/assets/storefront/src/i18n/server.ts +33 -5
  132. package/dist/assets/storefront/src/lib/backend-health.ts +11 -3
  133. package/dist/assets/storefront/src/lib/vendure/capability-documents.ts +246 -0
  134. package/dist/assets/storefront/src/lib/vendure/capability-schema.ts +358 -0
  135. package/dist/assets/storefront/src/lib/vendure/capability-ui.ts +15 -0
  136. package/dist/assets/storefront/src/lib/vendure/mutations.ts +835 -532
  137. package/dist/assets/storefront/src/lib/vendure/queries.ts +862 -519
  138. package/dist/assets/storefront/src/proxy.ts +11 -0
  139. package/dist/assets/storefront/src/schema.graphql +337 -243
  140. package/dist/assets/templates/README.md.hbs +25 -1
  141. package/dist/assets/templates/docker-compose.yml.hbs +4 -0
  142. package/dist/assets/templates/env.hbs +34 -9
  143. package/dist/assets/templates/environment.d.ts.hbs +1 -0
  144. package/dist/assets/templates/gitignore.hbs +7 -4
  145. package/dist/assets/templates/migrate.ts.hbs +55 -0
  146. package/dist/assets/templates/npmrc.hbs +7 -2
  147. package/dist/assets/templates/platform-edition.ts.hbs +21 -0
  148. package/dist/assets/templates/server-package.json.hbs +15 -8
  149. package/dist/assets/templates/vendure-config.ts.hbs +18 -0
  150. package/dist/bin/run.js +1 -1
  151. package/dist/cli.js +1 -1
  152. package/dist/console-onboarding.d.ts +44 -0
  153. package/dist/console-onboarding.js +1 -0
  154. package/dist/hub-projects.d.ts +48 -0
  155. package/dist/hub-projects.js +1 -0
  156. package/dist/index.d.ts +1 -1
  157. package/dist/index.js +1 -1
  158. package/dist/plugin-registry.d.ts +61 -2
  159. package/dist/plugin-registry.js +1 -1
  160. package/dist/registry.d.ts +16 -13
  161. package/dist/registry.js +1 -1
  162. package/dist/scaffold-shared.js +1 -1
  163. package/dist/scaffold-storefront-only.js +1 -1
  164. package/dist/scaffolder.d.ts +11 -0
  165. package/dist/scaffolder.js +1 -1
  166. package/dist/storefront-copier.d.ts +7 -2
  167. package/dist/storefront-copier.js +1 -1
  168. package/dist/template-renderer.d.ts +26 -5
  169. package/dist/template-renderer.js +1 -1
  170. package/dist/types.d.ts +54 -23
  171. package/package.json +14 -2
  172. package/dist/assets/storefront/src/middleware.ts +0 -11
@@ -1,6 +1,7 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
3
+ import "./.next/types/routes.d.ts";
4
+ import "./.next/types/root-params.d.ts";
4
5
 
5
6
  // NOTE: This file should not be edited
6
7
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -9,19 +9,21 @@ const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
9
9
  const nextConfig: NextConfig = {
10
10
  // cacheComponents enables PPR + useCache + dynamicIO.
11
11
  //
12
- // Combining this with next-intl requires rootParams so that 'use cache'
13
- // scopes can read the locale via next/root-params (which becomes an
14
- // automatic cache key) instead of headers() — which is forbidden in
15
- // cache boundaries.
12
+ // Combining this with next-intl needs next/root-params so that 'use cache'
13
+ // scopes can read the locale (which becomes an automatic cache key) instead
14
+ // of headers(), which is forbidden in cache boundaries. next/root-params is
15
+ // stable as of Next.js 16.3 and needs no config flag; the 16.2-era
16
+ // `experimental.rootParams` opt-in no longer exists.
16
17
  //
17
- // Key constraints (as of Next.js 16.2 / next-intl 4.9):
18
- // 1. No root layout above [locale] — the rootParams SWC loader walks
19
- // from the app root and stops at the first layout.tsx it finds.
20
- // If that layout isn't inside a dynamic segment, no root params
21
- // are generated and the next/root-params module exports nothing.
22
- // 2. next/root-params can only be imported in route files (layout,
23
- // page). Utility modules must use getLocale() or accept locale
24
- // as a parameter.
18
+ // Key constraints (as of Next.js 16.3 / next-intl 4.9):
19
+ // 1. No root layout above [locale]. Root params are the dynamic segments
20
+ // that sit above the root layout, so a layout.tsx at the app root
21
+ // would leave [locale] below it and next/root-params would export
22
+ // nothing.
23
+ // 2. next/root-params works in any Server Component, including shared
24
+ // utility modules, but not in Client Components, Server Actions or
25
+ // Route Handlers. Those must use getLocale() or take locale as a
26
+ // parameter — see src/i18n/server.ts.
25
27
  // 3. Turbopack dev has a pre-existing compilation hang on this project
26
28
  // (not i18n-related), so we use --webpack for dev and build.
27
29
  //
@@ -30,7 +32,6 @@ const nextConfig: NextConfig = {
30
32
  // https://aurorascharff.no/posts/implementing-nextjs-16-use-cache-with-next-intl-internationalization/
31
33
  cacheComponents: true,
32
34
  experimental: {
33
- rootParams: true,
34
35
  optimizePackageImports: ['lucide-react'],
35
36
  },
36
37
  env: {
@@ -37,16 +37,17 @@
37
37
  "@radix-ui/react-toggle": "^1.1.10",
38
38
  "@radix-ui/react-toggle-group": "^1.1.11",
39
39
  "@radix-ui/react-tooltip": "^1.2.8",
40
- "@vendure-platform/storefront-client": "^0.5.1",
40
+ "@vendure-platform/storefront-client": "^1.0.0-major.202609091053.0.0",
41
41
  "class-variance-authority": "^0.7.1",
42
42
  "clsx": "^2.1.1",
43
43
  "cmdk": "^1.1.1",
44
44
  "date-fns": "^4.1.0",
45
45
  "embla-carousel-react": "^8.6.0",
46
46
  "gql.tada": "^1.9.0",
47
+ "graphql": "^16.13.2",
47
48
  "input-otp": "^1.4.2",
48
49
  "lucide-react": "^0.544.0",
49
- "next": "^16.0.10",
50
+ "next": "^16.3.3",
50
51
  "next-intl": "^4.8.3",
51
52
  "next-themes": "^0.4.6",
52
53
  "react": " ^19.2.0",
@@ -1,4 +1,5 @@
1
1
  import { ErrorState } from '@/components/shared/error-state';
2
+ import { pinRootLocale } from '@/i18n/server';
2
3
  import { client } from '@/lib/vendure/api';
3
4
  import { GetAvailableCountriesQuery, GetCustomerAddressesQuery } from '@/lib/vendure/queries';
4
5
  import type { Metadata } from 'next';
@@ -11,6 +12,7 @@ export const metadata: Metadata = {
11
12
  };
12
13
 
13
14
  async function AddressesContent() {
15
+ await pinRootLocale();
14
16
  const t = await getTranslations('Account');
15
17
  const [addressesRes, countriesRes] = await Promise.all([
16
18
  client.query(GetCustomerAddressesQuery, {}),
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
6
6
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
7
7
  import { ResultOf } from '@/graphql';
8
8
  import { Link, redirect } from '@/i18n/navigation';
9
+ import { pinRootLocale } from '@/i18n/server';
9
10
  import { formatDateTime } from '@/lib/format';
10
11
  import { getActiveCustomer } from '@/lib/vendure/actions';
11
12
  import { client } from '@/lib/vendure/api';
@@ -330,6 +331,7 @@ async function ApprovalsContent({ params }: { params: Promise<{ locale: string }
330
331
  }
331
332
 
332
333
  export default async function ApprovalsPage({ params }: { params: Promise<{ locale: string }> }) {
334
+ await pinRootLocale();
333
335
  const t = await getTranslations('Approvals');
334
336
  return (
335
337
  <div>
@@ -1,6 +1,7 @@
1
1
  import { ErrorState } from '@/components/shared/error-state';
2
2
  import { readFragment } from '@/graphql';
3
3
  import { redirect } from '@/i18n/navigation';
4
+ import { pinRootLocale } from '@/i18n/server';
4
5
  import { getCompanyAccount } from '@/lib/vendure/actions';
5
6
  import { client } from '@/lib/vendure/api';
6
7
  import { CompanyBusinessUnitFragment } from '@/lib/vendure/fragments';
@@ -15,6 +16,7 @@ export const metadata: Metadata = {
15
16
  };
16
17
 
17
18
  async function BusinessUnitsContent() {
19
+ await pinRootLocale();
18
20
  const [t, companyAccount] = await Promise.all([
19
21
  getTranslations('Company.businessUnits'),
20
22
  getCompanyAccount(),
@@ -1,6 +1,7 @@
1
1
  import { CompanyStateBadge } from '@/components/commerce/company-state-badge';
2
2
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
3
3
  import { redirect } from '@/i18n/navigation';
4
+ import { pinRootLocale } from '@/i18n/server';
4
5
  import { getCompanyAccount, getMyCompanies } from '@/lib/vendure/actions';
5
6
  import { Clock } from 'lucide-react';
6
7
  import type { Metadata } from 'next';
@@ -13,6 +14,7 @@ export const metadata: Metadata = {
13
14
  };
14
15
 
15
16
  async function CompanyProfileContent() {
17
+ await pinRootLocale();
16
18
  const [t, companyAccount] = await Promise.all([getTranslations('Company'), getCompanyAccount()]);
17
19
 
18
20
  if (!companyAccount) {
@@ -1,6 +1,7 @@
1
1
  import { ErrorState } from '@/components/shared/error-state';
2
2
  import { readFragment } from '@/graphql';
3
3
  import { redirect } from '@/i18n/navigation';
4
+ import { pinRootLocale } from '@/i18n/server';
4
5
  import { getCompanyAccount } from '@/lib/vendure/actions';
5
6
  import { client } from '@/lib/vendure/api';
6
7
  import { CompanyPaymentMethodFragment } from '@/lib/vendure/fragments';
@@ -15,6 +16,7 @@ export const metadata: Metadata = {
15
16
  };
16
17
 
17
18
  async function CompanyPaymentMethodsContent() {
19
+ await pinRootLocale();
18
20
  const [t, companyAccount] = await Promise.all([
19
21
  getTranslations('Company.paymentMethods'),
20
22
  getCompanyAccount(),
@@ -1,5 +1,6 @@
1
1
  import { readFragment } from '@/graphql';
2
2
  import { redirect } from '@/i18n/navigation';
3
+ import { pinRootLocale } from '@/i18n/server';
3
4
  import { getCompanyAccount } from '@/lib/vendure/actions';
4
5
  import { client } from '@/lib/vendure/api';
5
6
  import { CompanyPermissionFragment, CompanyRoleFragment } from '@/lib/vendure/fragments';
@@ -14,6 +15,7 @@ export const metadata: Metadata = {
14
15
  };
15
16
 
16
17
  async function CompanyRolesContent() {
18
+ await pinRootLocale();
17
19
  const [t, companyAccount] = await Promise.all([getTranslations('Company.roles'), getCompanyAccount()]);
18
20
 
19
21
  if (!companyAccount) {
@@ -1,5 +1,6 @@
1
1
  import { readFragment } from '@/graphql';
2
2
  import { redirect } from '@/i18n/navigation';
3
+ import { pinRootLocale } from '@/i18n/server';
3
4
  import { getCompanyAccount } from '@/lib/vendure/actions';
4
5
  import { client } from '@/lib/vendure/api';
5
6
  import {
@@ -23,6 +24,7 @@ export const metadata: Metadata = {
23
24
  };
24
25
 
25
26
  async function CompanyUsersContent() {
27
+ await pinRootLocale();
26
28
  const [t, companyAccount] = await Promise.all([getTranslations('Company.users'), getCompanyAccount()]);
27
29
 
28
30
  if (!companyAccount) {
@@ -1,3 +1,4 @@
1
+ import { pinRootLocale } from '@/i18n/server';
1
2
  import { getActiveCustomer } from '@/lib/vendure/actions';
2
3
  import type { Metadata } from 'next';
3
4
  import { getTranslations } from 'next-intl/server';
@@ -11,6 +12,7 @@ export const metadata: Metadata = {
11
12
  };
12
13
 
13
14
  async function ProfileContent() {
15
+ await pinRootLocale();
14
16
  const [customer, t] = await Promise.all([getActiveCustomer(), getTranslations('Account')]);
15
17
 
16
18
  return (
@@ -1,4 +1,5 @@
1
1
  import { redirect } from '@/i18n/navigation';
2
+ import { pinRootLocale } from '@/i18n/server';
2
3
  import { getActiveCustomer } from '@/lib/vendure/actions';
3
4
  import type { Metadata } from 'next';
4
5
  import { getTranslations } from 'next-intl/server';
@@ -9,6 +10,7 @@ export const metadata: Metadata = {
9
10
  };
10
11
 
11
12
  export default async function QuickOrderPage() {
13
+ await pinRootLocale();
12
14
  const [customer, t] = await Promise.all([getActiveCustomer(), getTranslations('QuickOrder')]);
13
15
  if (!customer) {
14
16
  return await redirect('/sign-in');
@@ -8,12 +8,14 @@ import { QuoteVersionComparison } from '@/components/commerce/quote-version-comp
8
8
  import { Button } from '@/components/ui/button';
9
9
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
10
10
  import { Separator } from '@/components/ui/separator';
11
+ import { hasCapability } from '@/config/capabilities';
11
12
  import { Link, redirect } from '@/i18n/navigation';
12
13
  import { getActiveChannelToken } from '@/lib/channel-server';
13
14
  import { formatDate } from '@/lib/format';
14
15
  import { client } from '@/lib/vendure/api';
15
16
  import { getActiveChannelCached } from '@/lib/vendure/cached';
16
- import { GetMyQuoteDetailQuery } from '@/lib/vendure/queries';
17
+ import { getQuoteNegotiationData } from '@/lib/vendure/capability-ui';
18
+ import { GetMyQuoteDetailQuery, type QuoteComment, type QuoteVersion } from '@/lib/vendure/queries';
17
19
  import { AlertTriangle, ChevronLeft, Info } from 'lucide-react';
18
20
  import type { Metadata } from 'next';
19
21
  import { getTranslations, setRequestLocale } from 'next-intl/server';
@@ -105,10 +107,11 @@ async function QuoteDetailContent({
105
107
  return await redirect('/account/quotes');
106
108
  }
107
109
  const version = quote.currentVersion;
110
+ const negotiation = getQuoteNegotiationData<QuoteComment, QuoteVersion>(quote);
108
111
  const isTerminal = TERMINAL_STATUSES.has(quote.status);
109
- const v1 = quote.versions.find((v) => v.versionNumber === 1);
112
+ const v1 = negotiation.versions.find((item) => item.versionNumber === 1);
110
113
  const v1LineItems =
111
- v1 && quote.versions.length > 1
114
+ v1 && negotiation.versions.length > 1
112
115
  ? v1.lineItems.map((item) => ({ sku: item.sku, unitPrice: item.unitPrice }))
113
116
  : undefined;
114
117
 
@@ -167,7 +170,7 @@ async function QuoteDetailContent({
167
170
  {version && (
168
171
  <QuoteVersionComparison
169
172
  currentVersion={version}
170
- versions={quote.versions}
173
+ versions={negotiation.versions}
171
174
  currencyCode={currencyCode}
172
175
  />
173
176
  )}
@@ -223,8 +226,8 @@ async function QuoteDetailContent({
223
226
 
224
227
  {/* Activity Timeline */}
225
228
  <QuoteActivityTimeline
226
- versions={quote.versions}
227
- comments={quote.comments}
229
+ versions={negotiation.versions}
230
+ comments={negotiation.comments}
228
231
  currentVersionNumber={version?.versionNumber}
229
232
  currencyCode={currencyCode}
230
233
  />
@@ -276,6 +279,7 @@ async function QuoteDetailContent({
276
279
  quoteId={quote.id}
277
280
  status={quote.status}
278
281
  convertedOrderId={quote.convertedOrderId}
282
+ hasQuoteNegotiation={hasCapability('quote.negotiation')}
279
283
  />
280
284
  </CardContent>
281
285
  </Card>
@@ -18,8 +18,8 @@ type ActiveOrder = {
18
18
  shippingWithTax: number;
19
19
  total: number;
20
20
  totalWithTax: number;
21
- storeCreditsApplied: Array<{ code: string; balance: number; amountDeducted: number }>;
22
- totalWithTaxAfterStoreCredit: number;
21
+ storeCreditsApplied?: Array<{ code: string; balance: number; amountDeducted: number }>;
22
+ totalWithTaxAfterStoreCredit?: number;
23
23
  lines: Array<{
24
24
  id: string;
25
25
  quantity: number;
@@ -50,7 +50,8 @@ type ActiveOrder = {
50
50
 
51
51
  export async function OrderSummary({ activeOrder }: { activeOrder: ActiveOrder }) {
52
52
  const t = await getTranslations('Cart');
53
- const hasCreditsApplied = activeOrder.storeCreditsApplied.length > 0;
53
+ const storeCreditsApplied = activeOrder.storeCreditsApplied ?? [];
54
+ const hasCreditsApplied = storeCreditsApplied.length > 0;
54
55
  const authToken = await getAuthToken();
55
56
  const taxTotal = activeOrder.totalWithTax - activeOrder.total;
56
57
  const MIN_QTY_FOR_QUOTE = 50;
@@ -133,7 +134,7 @@ export async function OrderSummary({ activeOrder }: { activeOrder: ActiveOrder }
133
134
  </div>
134
135
  )}
135
136
  {hasCreditsApplied &&
136
- activeOrder.storeCreditsApplied.map((credit) => (
137
+ storeCreditsApplied.map((credit) => (
137
138
  <div key={credit.code} className="flex justify-between text-sm text-green-600">
138
139
  <span>{t('storeCredit', { code: maskCode(credit.code) })}</span>
139
140
  <span>
@@ -160,7 +161,9 @@ export async function OrderSummary({ activeOrder }: { activeOrder: ActiveOrder }
160
161
  />
161
162
  </span>
162
163
  <Price
163
- value={activeOrder.totalWithTaxAfterStoreCredit}
164
+ value={
165
+ activeOrder.totalWithTaxAfterStoreCredit ?? activeOrder.totalWithTax
166
+ }
164
167
  currencyCode={activeOrder.currencyCode}
165
168
  taxLabel="incl"
166
169
  />
@@ -15,8 +15,8 @@ const MAX_STORE_CREDITS_IN_CART = 100;
15
15
  type ActiveOrder = {
16
16
  id: string;
17
17
  couponCodes?: string[] | null;
18
- storeCreditsApplied: Array<{ code: string; balance: number; amountDeducted: number }>;
19
- totalWithTaxAfterStoreCredit: number;
18
+ storeCreditsApplied?: Array<{ code: string; balance: number; amountDeducted: number }>;
19
+ totalWithTaxAfterStoreCredit?: number;
20
20
  currencyCode: string;
21
21
  };
22
22
 
@@ -27,11 +27,14 @@ export async function PromotionCode({ activeOrder }: { activeOrder: ActiveOrder
27
27
  client.query(GetMyStoreCreditsQuery, { options: { take: MAX_STORE_CREDITS_IN_CART } }),
28
28
  ]);
29
29
  const availableCredits = creditsError ? [] : creditsResult.data.myStoreCredits?.items ?? [];
30
- const appliedCodes = new Set(activeOrder.storeCreditsApplied.map((c) => c.code));
30
+ const storeCreditsApplied = activeOrder.storeCreditsApplied ?? [];
31
+ const appliedCodes = new Set(storeCreditsApplied.map((c) => c.code));
31
32
  const activeCustomerId = activeCustomer?.id;
32
33
  // Only show credits the current customer can actually redeem (own + unclaimed)
33
34
  // Hide when order is already fully covered by applied credits
34
- const orderFullyCovered = activeOrder.totalWithTaxAfterStoreCredit <= 0;
35
+ const orderFullyCovered =
36
+ activeOrder.totalWithTaxAfterStoreCredit !== undefined &&
37
+ activeOrder.totalWithTaxAfterStoreCredit <= 0;
35
38
  const redeemableCredits = orderFullyCovered
36
39
  ? []
37
40
  : availableCredits.filter(
@@ -77,9 +80,9 @@ export async function PromotionCode({ activeOrder }: { activeOrder: ActiveOrder
77
80
  )}
78
81
 
79
82
  {/* Applied store credits */}
80
- {activeOrder.storeCreditsApplied.length > 0 && (
83
+ {storeCreditsApplied.length > 0 && (
81
84
  <div className="space-y-2">
82
- {activeOrder.storeCreditsApplied.map((credit) => (
85
+ {storeCreditsApplied.map((credit) => (
83
86
  <div
84
87
  key={credit.code}
85
88
  className="flex items-center justify-between p-3 border rounded-md bg-green-50 dark:bg-green-950/20"
@@ -1,6 +1,7 @@
1
1
  import { readFragment } from '@/graphql';
2
2
  import { redirect } from '@/i18n/navigation';
3
3
  import { type Locale } from '@/i18n/routing';
4
+ import { pinRootLocale } from '@/i18n/server';
4
5
  import { noIndexRobots } from '@/lib/metadata';
5
6
  import { getActiveCustomer, getCompanyAccount } from '@/lib/vendure/actions';
6
7
  import { client } from '@/lib/vendure/api';
@@ -145,6 +146,7 @@ async function CheckoutContent({ params }: { params: Promise<{ locale: string }>
145
146
  }
146
147
 
147
148
  export default async function CheckoutPage({ params }: { params: Promise<{ locale: string }> }) {
149
+ await pinRootLocale();
148
150
  const t = await getTranslations('Checkout');
149
151
  return (
150
152
  <div className="container mx-auto px-4 py-8 mt-[var(--page-top-offset)]">
@@ -15,9 +15,9 @@ import { getMessages, setRequestLocale } from 'next-intl/server';
15
15
  import { Geist, Geist_Mono, Inter_Tight, JetBrains_Mono } from 'next/font/google';
16
16
  import { Suspense } from 'react';
17
17
  import './globals.css';
18
- // next/root-params provides cache-safe locale resolution. The SWC transform
19
- // only runs in route files (layout/page), so this import cannot be moved to
20
- // a utility module. There must be no layout.tsx above [locale] — see next.config.ts.
18
+ // next/root-params provides cache-safe locale resolution. There must be no
19
+ // layout.tsx above [locale], or [locale] stops being a root param — see
20
+ // next.config.ts.
21
21
  import { toOgLocale } from '@/i18n/locale-utils';
22
22
  import { routing, type Locale } from '@/i18n/routing';
23
23
  import { hasLocale } from 'next-intl';
@@ -2,78 +2,20 @@ import { Price } from '@/components/commerce/price';
2
2
  import { Button } from '@/components/ui/button';
3
3
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
4
4
  import { Separator } from '@/components/ui/separator';
5
- import { graphql } from '@/graphql';
6
5
  import { Link } from '@/i18n/navigation';
7
6
  import { maskCode } from '@/lib/format';
8
7
  import { client } from '@/lib/vendure/api';
9
8
  import { getLatestApprovalForOrder } from '@/lib/vendure/approval-utils';
9
+ import { GetOrderByCodeForConfirmationQuery } from '@/lib/vendure/queries';
10
10
  import { CheckCircle2, Clock, MessageSquare, XCircle } from 'lucide-react';
11
11
  import { getTranslations } from 'next-intl/server';
12
12
  import Image from 'next/image';
13
13
  import { notFound } from 'next/navigation';
14
14
 
15
- const GetOrderByCodeQuery = graphql(`
16
- query GetOrderByCode($code: String!) {
17
- orderByCode(code: $code) {
18
- id
19
- code
20
- state
21
- subTotal
22
- subTotalWithTax
23
- shipping
24
- shippingWithTax
25
- total
26
- totalWithTax
27
- currencyCode
28
- storeCreditsApplied {
29
- code
30
- balance
31
- amountDeducted
32
- }
33
- totalWithTaxAfterStoreCredit
34
- payments {
35
- id
36
- method
37
- amount
38
- state
39
- metadata
40
- }
41
- lines {
42
- id
43
- productVariant {
44
- id
45
- name
46
- product {
47
- id
48
- name
49
- slug
50
- featuredAsset {
51
- id
52
- preview
53
- }
54
- }
55
- }
56
- quantity
57
- linePrice
58
- linePriceWithTax
59
- }
60
- shippingAddress {
61
- fullName
62
- streetLine1
63
- streetLine2
64
- city
65
- province
66
- postalCode
67
- country
68
- }
69
- }
70
- }
71
- `);
72
-
73
15
  export async function OrderConfirmation({ params }: { params: Promise<{ locale: string; code: string }> }) {
74
16
  const { code } = await params;
75
17
  const t = await getTranslations('OrderConfirmation');
76
- const [result, queryError] = await client.query(GetOrderByCodeQuery, { code });
18
+ const [result, queryError] = await client.query(GetOrderByCodeForConfirmationQuery, { code });
77
19
  if (queryError) {
78
20
  notFound();
79
21
  }
@@ -4,6 +4,7 @@ import { Price } from '@/components/commerce/price';
4
4
  import { Badge } from '@/components/ui/badge';
5
5
  import { FragmentOf, readFragment } from '@/graphql';
6
6
  import { Link } from '@/i18n/navigation';
7
+ import { getSearchHighlights } from '@/lib/vendure/capability-ui';
7
8
  import { ProductCardFragment } from '@/lib/vendure/fragments';
8
9
  import { ChevronDown, ChevronUp } from 'lucide-react';
9
10
  import Image from 'next/image';
@@ -19,6 +20,7 @@ interface ProductCardListProps {
19
20
 
20
21
  export function ProductCardList({ product: productProp, isExpanded, onToggle }: ProductCardListProps) {
21
22
  const product = readFragment(ProductCardFragment, productProp);
23
+ const highlights = getSearchHighlights(product);
22
24
  const prefetchedRef = useRef(false);
23
25
 
24
26
  const handleMouseEnter = () => {
@@ -70,10 +72,10 @@ export function ProductCardList({ product: productProp, isExpanded, onToggle }:
70
72
  >
71
73
  {product.productName}
72
74
  </Link>
73
- {product.highlights.length > 0 && product.highlights[0].matchedTokens.length > 0 && (
75
+ {highlights.length > 0 && highlights[0].matchedTokens.length > 0 && (
74
76
  <HighlightedText
75
77
  text={product.productName}
76
- matchedTokens={product.highlights[0].matchedTokens}
78
+ matchedTokens={highlights[0].matchedTokens}
77
79
  className="text-xs text-muted-foreground line-clamp-1 mt-0.5 red"
78
80
  />
79
81
  )}
@@ -2,6 +2,7 @@ import { Price } from '@/components/commerce/price';
2
2
  import { PrefetchLink } from '@/components/shared/prefetch-link';
3
3
  import { Badge } from '@/components/ui/badge';
4
4
  import { FragmentOf, readFragment } from '@/graphql';
5
+ import { getSearchHighlights } from '@/lib/vendure/capability-ui';
5
6
  import { ProductCardFragment } from '@/lib/vendure/fragments';
6
7
  import { Package } from 'lucide-react';
7
8
  import { useTranslations } from 'next-intl';
@@ -15,6 +16,7 @@ interface ProductCardProps {
15
16
 
16
17
  export function ProductCard({ product: productProp }: ProductCardProps) {
17
18
  const product = readFragment(ProductCardFragment, productProp);
19
+ const highlights = getSearchHighlights(product);
18
20
  const t = useTranslations('Product');
19
21
 
20
22
  return (
@@ -62,10 +64,10 @@ export function ProductCard({ product: productProp }: ProductCardProps) {
62
64
  <h3 className="font-medium text-sm line-clamp-2 leading-snug group-hover:text-primary transition-colors">
63
65
  {product.productName}
64
66
  </h3>
65
- {product.highlights.length > 0 && product.highlights[0].matchedTokens.length > 0 && (
67
+ {highlights.length > 0 && highlights[0].matchedTokens.length > 0 && (
66
68
  <HighlightedText
67
69
  text={product.productName}
68
- matchedTokens={product.highlights[0].matchedTokens}
70
+ matchedTokens={highlights[0].matchedTokens}
69
71
  className="text-xs text-muted-foreground line-clamp-2"
70
72
  />
71
73
  )}
@@ -23,9 +23,24 @@ interface QuoteActionButtonsProps {
23
23
  quoteId: string;
24
24
  status: string;
25
25
  convertedOrderId?: string | null;
26
+ /**
27
+ * Whether the deployment is granted `quote.negotiation`.
28
+ *
29
+ * Resolved by the server component that renders this one. Reading the
30
+ * capability hint here instead would inline the deployment's entire granted
31
+ * capability list into this client chunk, because Next.js replaces
32
+ * `process.env.NEXT_PUBLIC_*` with its literal value wherever the module
33
+ * graph reaches the browser.
34
+ */
35
+ hasQuoteNegotiation: boolean;
26
36
  }
27
37
 
28
- export function QuoteActionButtons({ quoteId, status, convertedOrderId }: QuoteActionButtonsProps) {
38
+ export function QuoteActionButtons({
39
+ quoteId,
40
+ status,
41
+ convertedOrderId,
42
+ hasQuoteNegotiation,
43
+ }: QuoteActionButtonsProps) {
29
44
  const router = useRouter();
30
45
  const t = useTranslations('Quotes');
31
46
  const [reason, setReason] = useState('');
@@ -174,7 +189,7 @@ export function QuoteActionButtons({ quoteId, status, convertedOrderId }: QuoteA
174
189
  )}
175
190
  {!stockWarning && (
176
191
  <>
177
- {status === 'QUOTED' && (
192
+ {status === 'QUOTED' && hasQuoteNegotiation && (
178
193
  <Textarea
179
194
  placeholder={t('changeRequestPlaceholder')}
180
195
  value={reason}
@@ -213,18 +228,20 @@ export function QuoteActionButtons({ quoteId, status, convertedOrderId }: QuoteA
213
228
  </Button>
214
229
  {status === 'QUOTED' && (
215
230
  <>
216
- <Button
217
- variant="outline"
218
- size="sm"
219
- onClick={() =>
220
- handleAction(() => requestQuoteChanges(quoteId, reason))
221
- }
222
- disabled={isPending || !reason.trim()}
223
- className="w-full"
224
- >
225
- <MessageSquare className="h-4 w-4 mr-1" />
226
- {t('actionRequestChanges')}
227
- </Button>
231
+ {hasQuoteNegotiation && (
232
+ <Button
233
+ variant="outline"
234
+ size="sm"
235
+ onClick={() =>
236
+ handleAction(() => requestQuoteChanges(quoteId, reason))
237
+ }
238
+ disabled={isPending || !reason.trim()}
239
+ className="w-full"
240
+ >
241
+ <MessageSquare className="h-4 w-4 mr-1" />
242
+ {t('actionRequestChanges')}
243
+ </Button>
244
+ )}
228
245
  <Button
229
246
  variant="destructive"
230
247
  size="sm"
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Build-time capability set for the storefront.
3
+ *
4
+ * The server composes its GraphQL schema from the capability modules the
5
+ * deployment is entitled to, so a document that selects a field belonging to an
6
+ * absent module fails validation on every request. The storefront therefore has
7
+ * to know which capability-gated fields it may select before it builds its
8
+ * documents.
9
+ *
10
+ * `NEXT_PUBLIC_VENDURE_CAPABILITIES` is a comma-separated list of the capability
11
+ * keys the target deployment is granted, baked at build time like
12
+ * `NEXT_PUBLIC_SCENARIO`. It is a composition hint, not an authorization
13
+ * boundary: the server remains authoritative and rejects anything this list
14
+ * gets wrong.
15
+ *
16
+ * When the variable is unset every capability is assumed, which keeps existing
17
+ * full-composition deployments building and running unchanged.
18
+ */
19
+ const RAW_CAPABILITIES = process.env.NEXT_PUBLIC_VENDURE_CAPABILITIES;
20
+
21
+ function parseCapabilities(raw: string | undefined): ReadonlySet<string> | null {
22
+ if (raw == null) {
23
+ return null;
24
+ }
25
+ return new Set(
26
+ raw
27
+ .split(',')
28
+ .map((key) => key.trim())
29
+ .filter((key) => key.length > 0),
30
+ );
31
+ }
32
+
33
+ const GRANTED = parseCapabilities(RAW_CAPABILITIES);
34
+
35
+ /** True when the deployment is expected to expose `key`'s schema. */
36
+ export function hasCapability(key: string): boolean {
37
+ return GRANTED == null || GRANTED.has(key);
38
+ }
39
+
40
+ /** Configurator forms, rules, saved configurations, and the configured order journey. */
41
+ export const hasConfiguratorForms = hasCapability('configurator.forms');