@forgecart/cli 2.202610280649.0 → 2.202610280926.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-questions.d.ts +12 -11
- package/dist/src/commands/init-questions.js +15 -23
- package/dist/src/commands/init-questions.js.map +1 -1
- package/dist/src/commands/init.d.ts +13 -13
- package/dist/src/commands/init.js +75 -144
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/refresh.d.ts +3 -3
- package/dist/src/commands/refresh.js +4 -14
- package/dist/src/commands/refresh.js.map +1 -1
- package/dist/src/commands/template-manifest.d.ts +2 -5
- package/dist/src/commands/template-manifest.js +1 -1
- package/dist/src/commands/template-manifest.js.map +1 -1
- package/dist/src/commands/type/init.input.d.ts +1 -1
- package/dist/src/commands/type/init.types.d.ts +4 -4
- package/dist/src/template/template-set.d.ts +17 -27
- package/dist/src/template/template-set.js +25 -42
- package/dist/src/template/template-set.js.map +1 -1
- package/dist/src/template/template-unavailable-error.d.ts +3 -4
- package/dist/src/template/template-unavailable-error.js +3 -4
- package/dist/src/template/template-unavailable-error.js.map +1 -1
- package/dist/src/template/type/template-set.types.d.ts +14 -17
- package/dist/src/template/type/template-set.types.js +6 -1
- package/dist/src/template/type/template-set.types.js.map +1 -1
- package/dist/src/template/util/template-identity.util.d.ts +1 -3
- package/dist/src/template/util/template-identity.util.js +0 -5
- package/dist/src/template/util/template-identity.util.js.map +1 -1
- package/package.json +1 -1
- package/templates/storefront-blank/.forgecartignore +2 -0
- package/templates/storefront-blank/Procfile +1 -0
- package/templates/storefront-blank/README.md +86 -0
- package/templates/storefront-blank/SEO-MIGRATION.md +718 -0
- package/templates/storefront-blank/_gitignore +21 -0
- package/templates/storefront-blank/next.config.js +105 -0
- package/templates/storefront-blank/package.json +35 -0
- package/templates/storefront-blank/postcss.config.js +5 -0
- package/templates/storefront-blank/src/app/%5F%5Ffc/identify/route.ts +205 -0
- package/templates/storefront-blank/src/app/%5F%5Ffc/track/route.ts +189 -0
- package/templates/storefront-blank/src/app/%5F%5Fforge_beacon/route.ts +87 -0
- package/templates/storefront-blank/src/app/api/%5F%5Fbackend/methods/route.ts +27 -0
- package/templates/storefront-blank/src/app/error.tsx +21 -0
- package/templates/storefront-blank/src/app/global-error.tsx +23 -0
- package/templates/storefront-blank/src/app/globals.css +110 -0
- package/templates/storefront-blank/src/app/layout.tsx +102 -0
- package/templates/storefront-blank/src/app/page.tsx +52 -0
- package/templates/storefront-blank/src/app/pages/[slug]/not-found.tsx +23 -0
- package/templates/storefront-blank/src/app/pages/[slug]/page.tsx +115 -0
- package/templates/storefront-blank/src/app/ping/route.ts +21 -0
- package/templates/storefront-blank/src/app/robots.ts +69 -0
- package/templates/storefront-blank/src/app/sitemap.ts +107 -0
- package/templates/storefront-blank/src/components/ForgeErrorBeacon.tsx +102 -0
- package/templates/storefront-blank/src/components/ForgeTracker.tsx +479 -0
- package/templates/storefront-blank/src/components/ForgecartDesigner.tsx +43 -0
- package/templates/storefront-blank/src/components/LocaleLink.tsx +49 -0
- package/templates/storefront-blank/src/instrumentation.ts +109 -0
- package/templates/storefront-blank/src/lib/account/account-link.ts +76 -0
- package/templates/storefront-blank/src/lib/account/account-state.ts +152 -0
- package/templates/storefront-blank/src/lib/account/register-state.ts +133 -0
- package/templates/storefront-blank/src/lib/account/reset-password-state.ts +111 -0
- package/templates/storefront-blank/src/lib/account/verify-state.ts +56 -0
- package/templates/storefront-blank/src/lib/account-actions.ts +76 -0
- package/templates/storefront-blank/src/lib/account-session.ts +114 -0
- package/templates/storefront-blank/src/lib/action-result.ts +30 -0
- package/templates/storefront-blank/src/lib/asset-alt.ts +34 -0
- package/templates/storefront-blank/src/lib/backend-actions.ts +20 -0
- package/templates/storefront-blank/src/lib/backend-client.ts +47 -0
- package/templates/storefront-blank/src/lib/cart-context.tsx +236 -0
- package/templates/storefront-blank/src/lib/checkout-session.ts +185 -0
- package/templates/storefront-blank/src/lib/content/page-metadata.ts +113 -0
- package/templates/storefront-blank/src/lib/content/render-fields.tsx +256 -0
- package/templates/storefront-blank/src/lib/content/resolve-page.ts +143 -0
- package/templates/storefront-blank/src/lib/error-messages.ts +118 -0
- package/templates/storefront-blank/src/lib/experiments.ts +333 -0
- package/templates/storefront-blank/src/lib/forgecart.ts +464 -0
- package/templates/storefront-blank/src/lib/format.ts +89 -0
- package/templates/storefront-blank/src/lib/identify-forward.ts +152 -0
- package/templates/storefront-blank/src/lib/locale/channel-locales-loader.ts +169 -0
- package/templates/storefront-blank/src/lib/locale/channel-locales-map.ts +46 -0
- package/templates/storefront-blank/src/lib/locale/channel-locales.ts +191 -0
- package/templates/storefront-blank/src/lib/locale/grammar.ts +194 -0
- package/templates/storefront-blank/src/lib/locale/localized-path.ts +55 -0
- package/templates/storefront-blank/src/lib/locale/middleware-plan.ts +107 -0
- package/templates/storefront-blank/src/lib/locale/request-binding.ts +80 -0
- package/templates/storefront-blank/src/lib/locale/request-locale.ts +66 -0
- package/templates/storefront-blank/src/lib/marketing-params.ts +213 -0
- package/templates/storefront-blank/src/lib/money.ts +50 -0
- package/templates/storefront-blank/src/lib/seo/alternates.ts +123 -0
- package/templates/storefront-blank/src/lib/seo/json-ld.ts +266 -0
- package/templates/storefront-blank/src/lib/seo/metadata.ts +419 -0
- package/templates/storefront-blank/src/lib/seo/noindex.ts +223 -0
- package/templates/storefront-blank/src/lib/seo/public-origin.ts +166 -0
- package/templates/storefront-blank/src/lib/seo/redirect-plan.ts +86 -0
- package/templates/storefront-blank/src/lib/seo/resolve-path.ts +107 -0
- package/templates/storefront-blank/src/lib/seo/scaffolded-routes.ts +83 -0
- package/templates/storefront-blank/src/lib/seo/sidecar.ts +75 -0
- package/templates/storefront-blank/src/lib/seo/site-verification.ts +98 -0
- package/templates/storefront-blank/src/lib/seo/sitemap-cache.ts +114 -0
- package/templates/storefront-blank/src/lib/seo/sitemap-entries.ts +310 -0
- package/templates/storefront-blank/src/lib/session-actions.ts +61 -0
- package/templates/storefront-blank/src/lib/session-cookies.ts +98 -0
- package/templates/storefront-blank/src/lib/shop-config.ts +51 -0
- package/templates/storefront-blank/src/lib/shop-session.ts +204 -0
- package/templates/storefront-blank/src/lib/track-forward.ts +200 -0
- package/templates/storefront-blank/src/lib/uuid.ts +19 -0
- package/templates/storefront-blank/src/middleware.ts +379 -0
- package/templates/storefront-blank/src/seo/redirects.ts +44 -0
- package/templates/storefront-blank/src/seo/static-routes.ts +12 -0
- package/templates/storefront-blank/src/server/app.module.ts +18 -0
- package/templates/storefront-blank/src/server/backend-api.ts +21 -0
- package/templates/storefront-blank/src/server/backend-method.decorator.ts +23 -0
- package/templates/storefront-blank/src/server/bootstrap.ts +122 -0
- package/templates/storefront-blank/src/server/forge/live-revision.ts +158 -0
- package/templates/storefront-blank/src/server/forgecart/forgecart-client.factory.ts +73 -0
- package/templates/storefront-blank/src/server/forgecart/forgecart.module.ts +9 -0
- package/templates/storefront-blank/src/server/runner.ts +90 -0
- package/templates/storefront-blank/src/server/types.ts +36 -0
- package/templates/storefront-blank/tsconfig.json +25 -0
- package/templates/storefront-blank-sdk-floor.json +1174 -0
- package/templates/storefront-shadcn-blank/.forgecartignore +2 -0
- package/templates/storefront-shadcn-blank/Procfile +1 -0
- package/templates/storefront-shadcn-blank/README.md +93 -0
- package/templates/storefront-shadcn-blank/SEO-MIGRATION.md +718 -0
- package/templates/storefront-shadcn-blank/_gitignore +21 -0
- package/templates/storefront-shadcn-blank/components.json +20 -0
- package/templates/storefront-shadcn-blank/next.config.js +105 -0
- package/templates/storefront-shadcn-blank/package.json +42 -0
- package/templates/storefront-shadcn-blank/postcss.config.js +5 -0
- package/templates/storefront-shadcn-blank/src/app/%5F%5Ffc/identify/route.ts +205 -0
- package/templates/storefront-shadcn-blank/src/app/%5F%5Ffc/track/route.ts +189 -0
- package/templates/storefront-shadcn-blank/src/app/%5F%5Fforge_beacon/route.ts +87 -0
- package/templates/storefront-shadcn-blank/src/app/api/%5F%5Fbackend/methods/route.ts +27 -0
- package/templates/storefront-shadcn-blank/src/app/error.tsx +23 -0
- package/templates/storefront-shadcn-blank/src/app/global-error.tsx +23 -0
- package/templates/storefront-shadcn-blank/src/app/globals.css +156 -0
- package/templates/storefront-shadcn-blank/src/app/layout.tsx +102 -0
- package/templates/storefront-shadcn-blank/src/app/page.tsx +53 -0
- package/templates/storefront-shadcn-blank/src/app/pages/[slug]/not-found.tsx +24 -0
- package/templates/storefront-shadcn-blank/src/app/pages/[slug]/page.tsx +115 -0
- package/templates/storefront-shadcn-blank/src/app/ping/route.ts +21 -0
- package/templates/storefront-shadcn-blank/src/app/robots.ts +69 -0
- package/templates/storefront-shadcn-blank/src/app/sitemap.ts +107 -0
- package/templates/storefront-shadcn-blank/src/components/ForgeErrorBeacon.tsx +102 -0
- package/templates/storefront-shadcn-blank/src/components/ForgeTracker.tsx +479 -0
- package/templates/storefront-shadcn-blank/src/components/ForgecartDesigner.tsx +43 -0
- package/templates/storefront-shadcn-blank/src/components/LocaleLink.tsx +49 -0
- package/templates/storefront-shadcn-blank/src/components/ui/alert.tsx +75 -0
- package/templates/storefront-shadcn-blank/src/components/ui/badge.tsx +40 -0
- package/templates/storefront-shadcn-blank/src/components/ui/button.tsx +64 -0
- package/templates/storefront-shadcn-blank/src/components/ui/card.tsx +28 -0
- package/templates/storefront-shadcn-blank/src/components/ui/input.tsx +26 -0
- package/templates/storefront-shadcn-blank/src/components/ui/label.tsx +22 -0
- package/templates/storefront-shadcn-blank/src/components/ui/native-select.tsx +27 -0
- package/templates/storefront-shadcn-blank/src/components/ui/skeleton.tsx +21 -0
- package/templates/storefront-shadcn-blank/src/components/ui/utils.ts +16 -0
- package/templates/storefront-shadcn-blank/src/instrumentation.ts +109 -0
- package/templates/storefront-shadcn-blank/src/lib/account/account-link.ts +76 -0
- package/templates/storefront-shadcn-blank/src/lib/account/account-state.ts +152 -0
- package/templates/storefront-shadcn-blank/src/lib/account/register-state.ts +133 -0
- package/templates/storefront-shadcn-blank/src/lib/account/reset-password-state.ts +111 -0
- package/templates/storefront-shadcn-blank/src/lib/account/verify-state.ts +56 -0
- package/templates/storefront-shadcn-blank/src/lib/account-actions.ts +76 -0
- package/templates/storefront-shadcn-blank/src/lib/account-session.ts +114 -0
- package/templates/storefront-shadcn-blank/src/lib/action-result.ts +30 -0
- package/templates/storefront-shadcn-blank/src/lib/asset-alt.ts +34 -0
- package/templates/storefront-shadcn-blank/src/lib/backend-actions.ts +20 -0
- package/templates/storefront-shadcn-blank/src/lib/backend-client.ts +47 -0
- package/templates/storefront-shadcn-blank/src/lib/cart-context.tsx +236 -0
- package/templates/storefront-shadcn-blank/src/lib/checkout-session.ts +185 -0
- package/templates/storefront-shadcn-blank/src/lib/content/page-metadata.ts +113 -0
- package/templates/storefront-shadcn-blank/src/lib/content/render-fields.tsx +256 -0
- package/templates/storefront-shadcn-blank/src/lib/content/resolve-page.ts +143 -0
- package/templates/storefront-shadcn-blank/src/lib/error-messages.ts +118 -0
- package/templates/storefront-shadcn-blank/src/lib/experiments.ts +333 -0
- package/templates/storefront-shadcn-blank/src/lib/forgecart.ts +464 -0
- package/templates/storefront-shadcn-blank/src/lib/format.ts +89 -0
- package/templates/storefront-shadcn-blank/src/lib/identify-forward.ts +152 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/channel-locales-loader.ts +169 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/channel-locales-map.ts +46 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/channel-locales.ts +191 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/grammar.ts +194 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/localized-path.ts +55 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/middleware-plan.ts +107 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/request-binding.ts +80 -0
- package/templates/storefront-shadcn-blank/src/lib/locale/request-locale.ts +66 -0
- package/templates/storefront-shadcn-blank/src/lib/marketing-params.ts +213 -0
- package/templates/storefront-shadcn-blank/src/lib/money.ts +50 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/alternates.ts +123 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/json-ld.ts +266 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/metadata.ts +419 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/noindex.ts +223 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/public-origin.ts +166 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/redirect-plan.ts +86 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/resolve-path.ts +107 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/scaffolded-routes.ts +83 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/sidecar.ts +75 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/site-verification.ts +98 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/sitemap-cache.ts +114 -0
- package/templates/storefront-shadcn-blank/src/lib/seo/sitemap-entries.ts +310 -0
- package/templates/storefront-shadcn-blank/src/lib/session-actions.ts +61 -0
- package/templates/storefront-shadcn-blank/src/lib/session-cookies.ts +98 -0
- package/templates/storefront-shadcn-blank/src/lib/shop-config.ts +51 -0
- package/templates/storefront-shadcn-blank/src/lib/shop-session.ts +204 -0
- package/templates/storefront-shadcn-blank/src/lib/track-forward.ts +200 -0
- package/templates/storefront-shadcn-blank/src/lib/uuid.ts +19 -0
- package/templates/storefront-shadcn-blank/src/middleware.ts +379 -0
- package/templates/storefront-shadcn-blank/src/seo/redirects.ts +44 -0
- package/templates/storefront-shadcn-blank/src/seo/static-routes.ts +12 -0
- package/templates/storefront-shadcn-blank/src/server/app.module.ts +18 -0
- package/templates/storefront-shadcn-blank/src/server/backend-api.ts +21 -0
- package/templates/storefront-shadcn-blank/src/server/backend-method.decorator.ts +23 -0
- package/templates/storefront-shadcn-blank/src/server/bootstrap.ts +122 -0
- package/templates/storefront-shadcn-blank/src/server/forge/live-revision.ts +158 -0
- package/templates/storefront-shadcn-blank/src/server/forgecart/forgecart-client.factory.ts +73 -0
- package/templates/storefront-shadcn-blank/src/server/forgecart/forgecart.module.ts +9 -0
- package/templates/storefront-shadcn-blank/src/server/runner.ts +90 -0
- package/templates/storefront-shadcn-blank/src/server/types.ts +36 -0
- package/templates/storefront-shadcn-blank/tsconfig.json +25 -0
- package/templates/storefront-shadcn-blank-sdk-floor.json +1174 -0
- package/templates/template-set.json +15 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { AccountLinkOutcome } from './account-link';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What `/verify` renders, as a value (#1472).
|
|
5
|
+
*
|
|
6
|
+
* PURE BY CONTRACT, for the reasons `account-link.ts` states. The page owns the
|
|
7
|
+
* COPY for each state; this module owns which state a given outcome is, so the
|
|
8
|
+
* decision is a table of vectors and the page is a render.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The states of the e-mail verification page.
|
|
13
|
+
*
|
|
14
|
+
* - `no-token` — the URL carried no token this page may act on.
|
|
15
|
+
* - `verified` — the account is now verified.
|
|
16
|
+
* - `link-invalid` — the API refused the token: already used, expired, or
|
|
17
|
+
* never issued. They are ONE state on purpose — the page's
|
|
18
|
+
* advice is identical ("request a new mail") and telling an
|
|
19
|
+
* unauthenticated visitor whether a token they do not hold
|
|
20
|
+
* once existed is an oracle with no upside.
|
|
21
|
+
* - `wrong-store` — the token was valid, but the account it belongs to is not
|
|
22
|
+
* a customer of THIS store.
|
|
23
|
+
* - `failed` — anything else: the shop API was unreachable, or answered
|
|
24
|
+
* a code this template does not model.
|
|
25
|
+
*/
|
|
26
|
+
export type VerifyState = 'no-token' | 'verified' | 'link-invalid' | 'wrong-store' | 'failed';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `ErrorCode` → state, for the codes `verifyCustomerAccount` can answer with.
|
|
30
|
+
*
|
|
31
|
+
* `CUSTOMER_NOT_FOUND` is the `wrong-store` case and the reason that state
|
|
32
|
+
* exists. Verification resolves the token GLOBALLY (the platform's auth method
|
|
33
|
+
* is keyed by the token alone), and only the customer lookup that follows is
|
|
34
|
+
* scoped to the channel this storefront serves — so a token issued by another
|
|
35
|
+
* store passes the first step and fails the second. That is exactly what a
|
|
36
|
+
* shopper holding an OLD mail hits: before #1472 every store's links pointed at
|
|
37
|
+
* one platform-global host, so a stale link can still arrive at the wrong
|
|
38
|
+
* storefront. "Something went wrong" would be true and useless; naming the
|
|
39
|
+
* cause is what lets the shopper act on it.
|
|
40
|
+
*
|
|
41
|
+
* Everything absent from this table lands in `failed` by design: an
|
|
42
|
+
* unrecognised code is a state this template has not been taught, and guessing
|
|
43
|
+
* at a friendlier one would put words in the API's mouth.
|
|
44
|
+
*/
|
|
45
|
+
const VERIFY_ERROR_STATES: Readonly<Record<string, VerifyState>> = {
|
|
46
|
+
VERIFICATION_TOKEN_INVALID: 'link-invalid',
|
|
47
|
+
VERIFICATION_TOKEN_EXPIRED: 'link-invalid',
|
|
48
|
+
CUSTOMER_NOT_FOUND: 'wrong-store',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** The state a verification render lands in. Total over the outcome union. */
|
|
52
|
+
export function verifyState(outcome: AccountLinkOutcome): VerifyState {
|
|
53
|
+
if (outcome.kind === 'no-token') return 'no-token';
|
|
54
|
+
if (outcome.kind === 'ok') return 'verified';
|
|
55
|
+
return VERIFY_ERROR_STATES[outcome.code] ?? 'failed';
|
|
56
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import 'server-only';
|
|
4
|
+
|
|
5
|
+
import { verificationResendState, type VerificationResendState } from './account/register-state';
|
|
6
|
+
import {
|
|
7
|
+
passwordResetRequestState,
|
|
8
|
+
resetPasswordState,
|
|
9
|
+
type PasswordResetRequestSubmitState,
|
|
10
|
+
type ResetPasswordSubmitState,
|
|
11
|
+
} from './account/reset-password-state';
|
|
12
|
+
import { requestCustomerVerification, requestPasswordReset, resetPassword } from './forgecart';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The Server Actions behind the account forms (#1472, #1471).
|
|
16
|
+
*
|
|
17
|
+
* These are the places in this template where a SHOPPER's input reaches the
|
|
18
|
+
* shop API from the server rather than over their own websocket, and that
|
|
19
|
+
* placement is deliberate: all three operations are unauthenticated by design
|
|
20
|
+
* (whoever holds the token, or the address, may call them), so putting them on
|
|
21
|
+
* the session socket would open a session for someone who has not shopped yet
|
|
22
|
+
* and attach a cart identity to a password reset. They run on the anonymous
|
|
23
|
+
* channel singleton in `lib/forgecart.ts` instead.
|
|
24
|
+
*
|
|
25
|
+
* The register SUBMIT is the one account operation that does NOT belong here,
|
|
26
|
+
* for the mirror image of that reason: it is session-scoped, and the session
|
|
27
|
+
* has to be the shopper's own. It lives in `lib/account-session.ts`, which
|
|
28
|
+
* states that argument in full.
|
|
29
|
+
*
|
|
30
|
+
* Each action returns the page STATE, not the API's envelope: the mapping from
|
|
31
|
+
* `ErrorCode` to state lives once, in `lib/account/reset-password-state.ts`,
|
|
32
|
+
* and a string union crosses the RSC boundary as itself. The client form only
|
|
33
|
+
* has to render what it is given.
|
|
34
|
+
*
|
|
35
|
+
* A Server Action is a public HTTP endpoint — a caller can reach these without
|
|
36
|
+
* ever loading the form — which is exactly why neither of them validates
|
|
37
|
+
* anything the API validates: the token and the password are judged by the
|
|
38
|
+
* shop API, and the form's own confirmation check is a UX affordance that
|
|
39
|
+
* never leaves the browser (see `checkNewPassword`). Abuse budgets (how often
|
|
40
|
+
* an address may be mailed) belong to the platform, which owns the mail — and
|
|
41
|
+
* for the verification resend it enforces one silently, which is why no state
|
|
42
|
+
* here names it (see {@link VerificationResendState}).
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/** Consume a reset token and set the new password. */
|
|
46
|
+
export async function submitPasswordReset(input: {
|
|
47
|
+
token: string;
|
|
48
|
+
password: string;
|
|
49
|
+
}): Promise<ResetPasswordSubmitState> {
|
|
50
|
+
return resetPasswordState(await resetPassword(input));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Ask for a reset link.
|
|
55
|
+
*
|
|
56
|
+
* Answers the same neutral state whether or not an account matched — see
|
|
57
|
+
* {@link passwordResetRequestState}.
|
|
58
|
+
*/
|
|
59
|
+
export async function submitPasswordResetRequest(
|
|
60
|
+
emailAddress: string,
|
|
61
|
+
): Promise<PasswordResetRequestSubmitState> {
|
|
62
|
+
return passwordResetRequestState(await requestPasswordReset(emailAddress));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Ask for the verification mail again (#1471).
|
|
67
|
+
*
|
|
68
|
+
* Answers the same neutral state whether or not the address has anything to
|
|
69
|
+
* verify, and whether or not the platform's throttle swallowed this particular
|
|
70
|
+
* request — see {@link VerificationResendState}.
|
|
71
|
+
*/
|
|
72
|
+
export async function submitVerificationRequest(
|
|
73
|
+
emailAddress: string,
|
|
74
|
+
): Promise<VerificationResendState> {
|
|
75
|
+
return verificationResendState(await requestCustomerVerification(emailAddress));
|
|
76
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { NativeAuthInput, RegisterCustomerInput } from '@forgecart/sdk/shop';
|
|
4
|
+
|
|
5
|
+
import type { AccountCallOutcome } from './account/account-link';
|
|
6
|
+
import { isSessionGone, type ActiveCustomerOutcome } from './account/account-state';
|
|
7
|
+
import type { RegisterOutcome } from './account/register-state';
|
|
8
|
+
import { replaceShopSession, runSessionOp } from './shop-session';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The account calls that run on the shopper's OWN websocket: registering
|
|
12
|
+
* (#1471), and signing in and out (#1093).
|
|
13
|
+
*
|
|
14
|
+
* Every other unauthenticated account call in this template is a Server Action
|
|
15
|
+
* running on the anonymous channel singleton (`lib/account-actions.ts`). These
|
|
16
|
+
* are not, and the placement is the design rather than a preference:
|
|
17
|
+
*
|
|
18
|
+
* 1. CORRECTNESS. `lib/forgecart.ts` memoizes ONE client per language for the
|
|
19
|
+
* whole Next process, so every visitor's Server Action shares it — and
|
|
20
|
+
* therefore shares its shop session. The shop API's register idempotency is
|
|
21
|
+
* SESSION-scoped (a session holds one registration claim, which is what
|
|
22
|
+
* turns a double-clicked submit into one account and one prompt instead of
|
|
23
|
+
* a refusal), so running it on a shared session would answer one shopper
|
|
24
|
+
* with another shopper's pending state. A sign-in on that client would be
|
|
25
|
+
* worse still: it would sign EVERY visitor in as whoever signed in last.
|
|
26
|
+
* These calls are only sound on a session that belongs to one person.
|
|
27
|
+
* 2. INTENT. Registering starts the shopper relationship, and the anonymous
|
|
28
|
+
* session this socket holds IS the cart identity that signing in later
|
|
29
|
+
* promotes — so the cart someone filled before registering, or before
|
|
30
|
+
* signing in, follows them into their account.
|
|
31
|
+
*
|
|
32
|
+
* That is the opposite placement from `/verify` and `/reset-password`, and
|
|
33
|
+
* deliberately so: those act on a one-shot token out of a mail, where opening a
|
|
34
|
+
* session for a visitor who has not shopped yet would attach a cart identity to
|
|
35
|
+
* a password reset. The verification RESEND is in that second category too — it
|
|
36
|
+
* needs nothing but an address — so it stays a Server Action.
|
|
37
|
+
*
|
|
38
|
+
* Like `runSessionOp`, none of these throws: a refusal is a typed code in the
|
|
39
|
+
* outcome, which is the ordinary case (a mistyped password, an address already
|
|
40
|
+
* registered), and the pure state tables in `lib/account/` stay the only things
|
|
41
|
+
* that read an ErrorCode.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
export async function registerCustomerAccount(
|
|
45
|
+
input: RegisterCustomerInput,
|
|
46
|
+
): Promise<RegisterOutcome> {
|
|
47
|
+
const result = await runSessionOp(
|
|
48
|
+
async (client) =>
|
|
49
|
+
(await client.customer.registerCustomerAccount({ input })).registerCustomerAccount,
|
|
50
|
+
);
|
|
51
|
+
if (!result.ok) return { kind: 'error', code: result.error.code };
|
|
52
|
+
return { kind: 'ok', verificationPending: result.data.verificationPending };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Sign the shopper in.
|
|
57
|
+
*
|
|
58
|
+
* Resolves only once this tab's socket AND the session cookies carry the
|
|
59
|
+
* session the sign-in minted (`replaceShopSession`), so the caller's very next
|
|
60
|
+
* operation — the greeting — runs as the signed-in shopper. Before #1093 every
|
|
61
|
+
* storefront hand-rolled this, and the one that did not rotate the socket had
|
|
62
|
+
* its next operation refused UNAUTHORIZED and reported a failed sign-in (#1089).
|
|
63
|
+
*
|
|
64
|
+
* A refusal resolves with its code — `INVALID_CREDENTIALS`, `NOT_VERIFIED`, … —
|
|
65
|
+
* and changes nothing: the socket keeps the session it had, cart included.
|
|
66
|
+
*/
|
|
67
|
+
export async function loginCustomer(input: NativeAuthInput): Promise<AccountCallOutcome> {
|
|
68
|
+
const result = await runSessionOp(async (client) => (await client.auth.login({ input })).login);
|
|
69
|
+
if (!result.ok) return { kind: 'error', code: result.error.code };
|
|
70
|
+
await replaceShopSession(result.data.token);
|
|
71
|
+
return { kind: 'ok' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sign the shopper out.
|
|
76
|
+
*
|
|
77
|
+
* Asks the shop API to end the session, then leaves this tab without one —
|
|
78
|
+
* the socket replaced by one that carries no session, both cookies cleared —
|
|
79
|
+
* WHATEVER the API answered: signing out is first a promise about this device,
|
|
80
|
+
* and a shopper on a shared computer who pressed the button must not stay
|
|
81
|
+
* signed in because the network blinked. The outcome reports the API's half:
|
|
82
|
+
* `ok` when the session is over (a session the API no longer holds included),
|
|
83
|
+
* the refusal's code when the API could not confirm it.
|
|
84
|
+
*/
|
|
85
|
+
export async function logoutCustomer(): Promise<AccountCallOutcome> {
|
|
86
|
+
const ended = await runSessionOp(async (client) => (await client.auth.logout()).logout);
|
|
87
|
+
await replaceShopSession(null);
|
|
88
|
+
if (ended.ok || isSessionGone(ended.error.code)) return { kind: 'ok' };
|
|
89
|
+
return { kind: 'error', code: ended.error.code };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Who this tab's session belongs to.
|
|
94
|
+
*
|
|
95
|
+
* A socket carrying no session is answered without a request — a visitor who
|
|
96
|
+
* has never shopped, or one who just signed out, belongs to nobody — and so is
|
|
97
|
+
* every page load that finds no mirror cookie, since the socket boots its
|
|
98
|
+
* session from it. Otherwise the shop API is asked, and an anonymous cart
|
|
99
|
+
* session answers `null` too.
|
|
100
|
+
*/
|
|
101
|
+
export async function findSignedInCustomer(): Promise<ActiveCustomerOutcome> {
|
|
102
|
+
const result = await runSessionOp(async (client) => {
|
|
103
|
+
if (client.getAuthToken() === null) return null;
|
|
104
|
+
const customer = (await client.customer.activeCustomer()).activeCustomer ?? null;
|
|
105
|
+
if (customer === null) return null;
|
|
106
|
+
return {
|
|
107
|
+
firstName: customer.firstName,
|
|
108
|
+
lastName: customer.lastName,
|
|
109
|
+
emailAddress: customer.emailAddress,
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
if (!result.ok) return { kind: 'error', code: result.error.code };
|
|
113
|
+
return { kind: 'ok', customer: result.data };
|
|
114
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ExtractedError } from '@forgecart/sdk/shop';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Typed result envelope for every Server Action in this template.
|
|
5
|
+
*
|
|
6
|
+
* Server Actions NEVER throw across the RSC boundary: Next.js production
|
|
7
|
+
* redacts a thrown action error to an opaque digest, so the client would see
|
|
8
|
+
* a generic failure with no code to render. Instead each action catches,
|
|
9
|
+
* translates the failure with the SDK's `extractError` (the shop API's
|
|
10
|
+
* `{ code, variables, classification, message }` extensions — `message`
|
|
11
|
+
* arrives server-localized), and returns it in-band. Callers switch on
|
|
12
|
+
* `ok`; error UIs render `error.message` and key field targeting off
|
|
13
|
+
* `error.code`.
|
|
14
|
+
*
|
|
15
|
+
* The `import type` above erases, so this module is safe for client
|
|
16
|
+
* components — it is the one shape both sides of the action boundary share.
|
|
17
|
+
*/
|
|
18
|
+
export type ActionResult<T> = { ok: true; data: T } | { ok: false; error: ExtractedError };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fallback envelope error for failures that carry no GraphQL extensions
|
|
22
|
+
* (network refused, DNS, a non-GraphQL throw). `extractError` returns `null`
|
|
23
|
+
* for those; actions substitute this so the client always receives a code it
|
|
24
|
+
* can key on.
|
|
25
|
+
*/
|
|
26
|
+
export const UNREACHABLE_ERROR: ExtractedError = {
|
|
27
|
+
code: 'SHOP_API_UNREACHABLE',
|
|
28
|
+
variables: {},
|
|
29
|
+
classification: 'INTERNAL_ERROR',
|
|
30
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `alt` attribute for an asset-backed `<img>` — the one place the template
|
|
3
|
+
* decides what an undescribed image announces (#1376).
|
|
4
|
+
*
|
|
5
|
+
* Alt text is the merchant's OWN description of the image, authored in the
|
|
6
|
+
* dashboard's media library. Its absence is therefore a decision rather than a
|
|
7
|
+
* gap: an image nobody described is announced as decorative (`alt=""`) and
|
|
8
|
+
* skipped by screen readers, which is exactly right for the product shots,
|
|
9
|
+
* badges and thumbnails that repeat text already sitting next to them.
|
|
10
|
+
*
|
|
11
|
+
* It is deliberately NEVER back-filled with the product or variant name. A
|
|
12
|
+
* derived placeholder is indistinguishable from a real description to both a
|
|
13
|
+
* screen reader and a crawler, while carrying nothing the adjacent markup does
|
|
14
|
+
* not already say — so it spends a listener's attention repeating the caption
|
|
15
|
+
* they just heard, and tells a crawler the image was described when it was
|
|
16
|
+
* not. The empty string is the honest answer and the accessible one, and this
|
|
17
|
+
* function exists so that judgement is stated once instead of re-derived at
|
|
18
|
+
* every `<img>` (all four of which read `alt={product.name}` before it).
|
|
19
|
+
*
|
|
20
|
+
* A blank-but-present description collapses to the same decorative default.
|
|
21
|
+
* `alt=" "` is the worst of both worlds — not a description, and not the
|
|
22
|
+
* empty string that marks an image skippable, so a reader falls back to
|
|
23
|
+
* announcing the file name. The dashboard already trims before it writes, but
|
|
24
|
+
* the field is equally writable through the admin API, so the guarantee is
|
|
25
|
+
* made here where it is actually rendered rather than assumed upstream.
|
|
26
|
+
*
|
|
27
|
+
* The parameter is structural, not the SDK's `Asset`: every selection that
|
|
28
|
+
* renders an image picks `altText` alongside `preview`, and asking only for
|
|
29
|
+
* the field actually read keeps this module free of the SDK import — which is
|
|
30
|
+
* what lets it be unit-tested directly (see tool/storefront-template-spec).
|
|
31
|
+
*/
|
|
32
|
+
export function getAssetAlt(asset: { altText?: string | null }): string {
|
|
33
|
+
return asset.altText?.trim() ?? '';
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import 'server-only';
|
|
4
|
+
|
|
5
|
+
import { invokeBackendMethod } from '../server/runner';
|
|
6
|
+
import type { ActionResult } from './action-result';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The ONE Server Action bridging the browser into the embedded NestJS
|
|
10
|
+
* backend (`src/server/`) — Next's native RPC is the transport, the booted
|
|
11
|
+
* registry is the authority. Components never import this directly: they
|
|
12
|
+
* call through `backend` (`lib/backend-client.ts`), which types the surface
|
|
13
|
+
* off `backend-api.ts` and guards the proxy's magic keys.
|
|
14
|
+
*/
|
|
15
|
+
export async function invokeBackend(
|
|
16
|
+
method: string,
|
|
17
|
+
input: unknown,
|
|
18
|
+
): Promise<ActionResult<unknown>> {
|
|
19
|
+
return invokeBackendMethod(method, input);
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { BackendApi } from '../server/backend-api';
|
|
2
|
+
import type { ActionResult } from './action-result';
|
|
3
|
+
import { invokeBackend } from './backend-actions';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `sdk.backend` — the typed browser gate onto the storefront's embedded
|
|
7
|
+
* NestJS backend.
|
|
8
|
+
*
|
|
9
|
+
* Compile time: the mapped type below projects `backend-api.ts` onto the
|
|
10
|
+
* proxy, so editors autocomplete exactly the registered methods and an
|
|
11
|
+
* unknown name fails the build. (The import is type-only — it erases, so no
|
|
12
|
+
* server code reaches the client bundle; the `server-only` guards enforce
|
|
13
|
+
* it.)
|
|
14
|
+
*
|
|
15
|
+
* Runtime: the proxy fabricates an invoker for ANY string key — the
|
|
16
|
+
* server-side registry is the real authority, answering
|
|
17
|
+
* BACKEND_METHOD_NOT_FOUND in the normal envelope for unknown or
|
|
18
|
+
* stale-bundle names. Magic keys are excluded so the object never
|
|
19
|
+
* masquerades as a thenable (`await backend` must not fire a wire call
|
|
20
|
+
* named "then").
|
|
21
|
+
*
|
|
22
|
+
* Usage, any client component:
|
|
23
|
+
* const result = await backend.assignCustomerHash({});
|
|
24
|
+
* if (!result.ok) return renderError(result.error);
|
|
25
|
+
*/
|
|
26
|
+
type BackendGate = {
|
|
27
|
+
[K in keyof BackendApi]: (
|
|
28
|
+
input: Parameters<BackendApi[K]>[0],
|
|
29
|
+
) => Promise<ActionResult<Awaited<ReturnType<BackendApi[K]>>>>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const RESERVED_KEYS = new Set([
|
|
33
|
+
'then',
|
|
34
|
+
'catch',
|
|
35
|
+
'finally',
|
|
36
|
+
'toJSON',
|
|
37
|
+
'valueOf',
|
|
38
|
+
'constructor',
|
|
39
|
+
'$$typeof',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export const backend: BackendGate = new Proxy({} as BackendGate, {
|
|
43
|
+
get(_target, key) {
|
|
44
|
+
if (typeof key !== 'string' || RESERVED_KEYS.has(key)) return undefined;
|
|
45
|
+
return (input: unknown) => invokeBackend(key, input);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ExtractedError } from '@forgecart/sdk/shop';
|
|
4
|
+
import type { ShopAddItemToOrderInput } from '@forgecart/sdk/shop';
|
|
5
|
+
|
|
6
|
+
import type { ActionResult } from './action-result';
|
|
7
|
+
import type { Order } from './forgecart';
|
|
8
|
+
import {
|
|
9
|
+
createContext,
|
|
10
|
+
useCallback,
|
|
11
|
+
useContext,
|
|
12
|
+
useEffect,
|
|
13
|
+
useMemo,
|
|
14
|
+
useState,
|
|
15
|
+
useTransition,
|
|
16
|
+
type ReactNode,
|
|
17
|
+
} from 'react';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
initShopSession,
|
|
21
|
+
isSessionReady,
|
|
22
|
+
readMirrorSession,
|
|
23
|
+
runSessionOp,
|
|
24
|
+
type ShopSessionConfig,
|
|
25
|
+
} from './shop-session';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Cart state on the shopper's OWN shop websocket (`shop-session.ts`) — the
|
|
29
|
+
* client half of the progressive stack.
|
|
30
|
+
*
|
|
31
|
+
* PPR-compatible boot: the server shell renders with NO cart (reading the
|
|
32
|
+
* session cookie would dynamize the whole layout), so the provider hydrates
|
|
33
|
+
* client-side. On mount it arms the session singleton with the serialized
|
|
34
|
+
* channel config; when the mirror cookie says a session exists, it reads the
|
|
35
|
+
* active order over the socket (`activeOrder` is nullable and NEVER mints —
|
|
36
|
+
* carts start with the first added item). `hydrated` stays false until that
|
|
37
|
+
* first read settles, so cart surfaces can render a skeleton instead of a
|
|
38
|
+
* false "empty cart" flash.
|
|
39
|
+
*
|
|
40
|
+
* Every mutation runs on the same socket and replaces `cart` with the order
|
|
41
|
+
* from the typed `ActionResult` envelope; a failure keeps the last known
|
|
42
|
+
* cart AND surfaces the typed error on `error`. `pending` is true while a
|
|
43
|
+
* cart mutation is in flight. The first `add` on a fresh visitor mints the
|
|
44
|
+
* session client-side; `shop-session.ts` syncs the captured token back into
|
|
45
|
+
* the cookies so the next SSR pass sees this cart.
|
|
46
|
+
*/
|
|
47
|
+
interface CartContextValue {
|
|
48
|
+
cart: Order | null;
|
|
49
|
+
itemCount: number;
|
|
50
|
+
subtotal: number;
|
|
51
|
+
/** False until the mount-time cart read settles — render skeletons, not "empty". */
|
|
52
|
+
hydrated: boolean;
|
|
53
|
+
pending: boolean;
|
|
54
|
+
error: ExtractedError | null;
|
|
55
|
+
add: (variantId: string, quantity?: number, sellingPlanId?: string) => void;
|
|
56
|
+
setQuantity: (lineId: string, quantity: number) => void;
|
|
57
|
+
remove: (lineId: string) => void;
|
|
58
|
+
setSellingPlan: (sellingPlanId: string | null) => void;
|
|
59
|
+
applyCoupon: (code: string) => void;
|
|
60
|
+
removeCoupon: (code: string) => void;
|
|
61
|
+
clearError: () => void;
|
|
62
|
+
refresh: () => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const CartContext = createContext<CartContextValue | null>(null);
|
|
66
|
+
|
|
67
|
+
export function CartProvider({
|
|
68
|
+
shopConfig,
|
|
69
|
+
children,
|
|
70
|
+
}: {
|
|
71
|
+
shopConfig: ShopSessionConfig | null;
|
|
72
|
+
children: ReactNode;
|
|
73
|
+
}) {
|
|
74
|
+
const [cart, setCart] = useState<Order | null>(null);
|
|
75
|
+
const [hydrated, setHydrated] = useState(false);
|
|
76
|
+
const [error, setError] = useState<ExtractedError | null>(null);
|
|
77
|
+
const [pending, startTransition] = useTransition();
|
|
78
|
+
|
|
79
|
+
const run = useCallback((op: () => Promise<ActionResult<Order | null>>) => {
|
|
80
|
+
startTransition(async () => {
|
|
81
|
+
const result = await op();
|
|
82
|
+
if (result.ok) {
|
|
83
|
+
setCart(result.data);
|
|
84
|
+
setError(null);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
setError(result.error);
|
|
88
|
+
});
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
// Arm the session singleton and hydrate the cart. An unconfigured scaffold
|
|
92
|
+
// (pre-`forgecart init`) or a visitor with no session hydrates to the
|
|
93
|
+
// legitimate empty state without touching the network.
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!shopConfig) {
|
|
96
|
+
setHydrated(true);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
initShopSession(shopConfig);
|
|
100
|
+
if (!readMirrorSession()) {
|
|
101
|
+
setHydrated(true);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
let cancelled = false;
|
|
105
|
+
void (async () => {
|
|
106
|
+
const result = await runSessionOp(
|
|
107
|
+
async (client) => (await client.order.activeOrder()).activeOrder ?? null,
|
|
108
|
+
);
|
|
109
|
+
if (cancelled) return;
|
|
110
|
+
if (result.ok) setCart(result.data);
|
|
111
|
+
setHydrated(true);
|
|
112
|
+
})();
|
|
113
|
+
return () => {
|
|
114
|
+
cancelled = true;
|
|
115
|
+
};
|
|
116
|
+
}, [shopConfig]);
|
|
117
|
+
|
|
118
|
+
const add = useCallback(
|
|
119
|
+
(variantId: string, quantity = 1, sellingPlanId?: string) =>
|
|
120
|
+
run(async () => {
|
|
121
|
+
const input: ShopAddItemToOrderInput = { variantId, quantity };
|
|
122
|
+
if (sellingPlanId) {
|
|
123
|
+
input.sellingPlanId = sellingPlanId;
|
|
124
|
+
}
|
|
125
|
+
return runSessionOp(
|
|
126
|
+
async (client) => (await client.cart.addItemToOrder({ input })).addItemToOrder,
|
|
127
|
+
);
|
|
128
|
+
}),
|
|
129
|
+
[run],
|
|
130
|
+
);
|
|
131
|
+
const setQuantity = useCallback(
|
|
132
|
+
(lineId: string, quantity: number) =>
|
|
133
|
+
run(() =>
|
|
134
|
+
runSessionOp(
|
|
135
|
+
async (client) =>
|
|
136
|
+
(await client.cart.adjustOrderLine({ input: { lineId, quantity } })).adjustOrderLine,
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
[run],
|
|
140
|
+
);
|
|
141
|
+
const remove = useCallback(
|
|
142
|
+
(lineId: string) =>
|
|
143
|
+
run(() =>
|
|
144
|
+
runSessionOp(
|
|
145
|
+
async (client) => (await client.cart.removeOrderLine({ lineId })).removeOrderLine,
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
[run],
|
|
149
|
+
);
|
|
150
|
+
const setSellingPlan = useCallback(
|
|
151
|
+
(sellingPlanId: string | null) =>
|
|
152
|
+
run(() =>
|
|
153
|
+
runSessionOp(
|
|
154
|
+
async (client) =>
|
|
155
|
+
(await client.cart.setOrderSellingPlan({ input: { sellingPlanId } }))
|
|
156
|
+
.setOrderSellingPlan,
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
[run],
|
|
160
|
+
);
|
|
161
|
+
const applyCoupon = useCallback(
|
|
162
|
+
(code: string) =>
|
|
163
|
+
run(() =>
|
|
164
|
+
runSessionOp(
|
|
165
|
+
async (client) => (await client.cart.applyCouponCode({ code })).applyCouponCode,
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
[run],
|
|
169
|
+
);
|
|
170
|
+
const removeCoupon = useCallback(
|
|
171
|
+
(code: string) =>
|
|
172
|
+
run(() =>
|
|
173
|
+
runSessionOp(
|
|
174
|
+
async (client) => (await client.cart.removeCouponCode({ code })).removeCouponCode,
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
[run],
|
|
178
|
+
);
|
|
179
|
+
const clearError = useCallback(() => setError(null), []);
|
|
180
|
+
const refresh = useCallback(
|
|
181
|
+
() =>
|
|
182
|
+
run(async () => {
|
|
183
|
+
if (!isSessionReady() || !readMirrorSession()) {
|
|
184
|
+
return { ok: true, data: null };
|
|
185
|
+
}
|
|
186
|
+
return runSessionOp(
|
|
187
|
+
async (client) => (await client.order.activeOrder()).activeOrder ?? null,
|
|
188
|
+
);
|
|
189
|
+
}),
|
|
190
|
+
[run],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const value = useMemo<CartContextValue>(
|
|
194
|
+
() => ({
|
|
195
|
+
cart,
|
|
196
|
+
itemCount: cart?.totalQuantity ?? 0,
|
|
197
|
+
subtotal: cart?.subTotalWithTax ?? 0,
|
|
198
|
+
hydrated,
|
|
199
|
+
pending,
|
|
200
|
+
error,
|
|
201
|
+
add,
|
|
202
|
+
setQuantity,
|
|
203
|
+
remove,
|
|
204
|
+
setSellingPlan,
|
|
205
|
+
applyCoupon,
|
|
206
|
+
removeCoupon,
|
|
207
|
+
clearError,
|
|
208
|
+
refresh,
|
|
209
|
+
}),
|
|
210
|
+
[
|
|
211
|
+
cart,
|
|
212
|
+
hydrated,
|
|
213
|
+
pending,
|
|
214
|
+
error,
|
|
215
|
+
add,
|
|
216
|
+
setQuantity,
|
|
217
|
+
remove,
|
|
218
|
+
setSellingPlan,
|
|
219
|
+
applyCoupon,
|
|
220
|
+
removeCoupon,
|
|
221
|
+
clearError,
|
|
222
|
+
refresh,
|
|
223
|
+
],
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Access the cart. Must be used inside a `<CartProvider>`. */
|
|
230
|
+
export function useCart(): CartContextValue {
|
|
231
|
+
const ctx = useContext(CartContext);
|
|
232
|
+
if (!ctx) {
|
|
233
|
+
throw new Error('useCart must be used within a CartProvider');
|
|
234
|
+
}
|
|
235
|
+
return ctx;
|
|
236
|
+
}
|