@vtex/faststore 0.0.1 → 0.0.3
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/CHANGELOG.md +35 -0
- package/README.md +4 -2
- package/dist/catalog.json +286 -265
- package/dist/components.d.ts +2 -1
- package/dist/components.js +2 -1
- package/dist/content-types.json +16 -0
- package/dist/core/dist/actions/registry.d.ts +1 -1
- package/dist/core/dist/actions/registry.js +1 -1
- package/dist/core/dist/actions/registry.js.map +1 -1
- package/dist/docs/account-entry-point.md +1 -1
- package/dist/docs/cart.md +2 -2
- package/dist/docs/session.md +12 -6
- package/dist/docs/sign-in-redirect.md +137 -0
- package/dist/matter/access-code-sign-in/access-code-sign-in.d.ts +2 -2
- package/dist/matter/access-code-sign-in/access-code-sign-in.js +5 -2
- package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
- package/dist/matter/account-entry-point/account-entry-point.js +4 -16
- package/dist/matter/account-entry-point/account-entry-point.js.map +1 -1
- package/dist/matter/buy-button/buy-button.d.ts +1 -1
- package/dist/matter/buy-button/buy-button.js +3 -1
- package/dist/matter/buy-button/buy-button.js.map +1 -1
- package/dist/matter/cart/cart.d.ts +6 -6
- package/dist/matter/cart/cart.js +13 -6
- package/dist/matter/cart/cart.js.map +1 -1
- package/dist/matter/cart-item/cart-item.d.ts +2 -2
- package/dist/matter/cart-item/cart-item.js +5 -2
- package/dist/matter/cart-item/cart-item.js.map +1 -1
- package/dist/matter/client-preferences/client-preferences.js +3 -1
- package/dist/matter/client-preferences/client-preferences.js.map +1 -1
- package/dist/matter/client-profile/client-profile.d.ts +1 -1
- package/dist/matter/client-profile/client-profile.js +3 -1
- package/dist/matter/client-profile/client-profile.js.map +1 -1
- package/dist/matter/client-profile-form/client-profile-form.d.ts +1 -1
- package/dist/matter/client-profile-form/client-profile-form.js +3 -1
- package/dist/matter/client-profile-form/client-profile-form.js.map +1 -1
- package/dist/matter/delivery-options/delivery-options-accordion.d.ts +1 -1
- package/dist/matter/delivery-options/delivery-options-accordion.js +3 -1
- package/dist/matter/delivery-options/delivery-options-accordion.js.map +1 -1
- package/dist/matter/delivery-options/delivery-options-controller.d.ts +1 -1
- package/dist/matter/delivery-options/delivery-options-controller.js +3 -1
- package/dist/matter/delivery-options/delivery-options-controller.js.map +1 -1
- package/dist/matter/delivery-options/delivery-options.d.ts +1 -1
- package/dist/matter/delivery-options/delivery-options.js +3 -1
- package/dist/matter/delivery-options/delivery-options.js.map +1 -1
- package/dist/matter/email-identification/email-identification.d.ts +1 -1
- package/dist/matter/email-identification/email-identification.js +3 -1
- package/dist/matter/email-identification/email-identification.js.map +1 -1
- package/dist/matter/identity-summary/identity-summary.d.ts +1 -1
- package/dist/matter/identity-summary/identity-summary.js +3 -1
- package/dist/matter/identity-summary/identity-summary.js.map +1 -1
- package/dist/matter/product-list/product-list.d.ts +1 -1
- package/dist/matter/product-list/product-list.js +3 -1
- package/dist/matter/product-list/product-list.js.map +1 -1
- package/dist/matter/region/region.d.ts +1 -1
- package/dist/matter/region/region.js +5 -2
- package/dist/matter/region/region.js.map +1 -1
- package/dist/matter/session/session.js +1 -1
- package/dist/matter/session/session.js.map +1 -1
- package/dist/matter/shipping-address/shipping-address.d.ts +1 -1
- package/dist/matter/shipping-address/shipping-address.js +3 -1
- package/dist/matter/shipping-address/shipping-address.js.map +1 -1
- package/dist/matter/sign-in-form/sign-in-form.d.ts +1 -1
- package/dist/matter/sign-in-form/sign-in-form.js +3 -1
- package/dist/matter/sign-in-form/sign-in-form.js.map +1 -1
- package/dist/matter/sign-in-redirect/sign-in-redirect.d.ts +45 -0
- package/dist/matter/sign-in-redirect/sign-in-redirect.js +127 -0
- package/dist/matter/sign-in-redirect/sign-in-redirect.js.map +1 -0
- package/dist/sdk/actions/actions-override-context.js +37 -0
- package/dist/sdk/actions/actions-override-context.js.map +1 -0
- package/dist/sdk/actions/cart-action-provider.d.ts +8 -6
- package/dist/sdk/actions/cart-action-provider.js +10 -8
- package/dist/sdk/actions/cart-action-provider.js.map +1 -1
- package/dist/sdk/actions/identity-action-provider.d.ts +2 -2
- package/dist/sdk/actions/identity-action-provider.js +4 -4
- package/dist/sdk/actions/identity-action-provider.js.map +1 -1
- package/dist/sdk/actions/inject-actions.d.ts +1 -5
- package/dist/sdk/actions/inject-actions.js +23 -21
- package/dist/sdk/actions/inject-actions.js.map +1 -1
- package/dist/sdk/actions/region-action-provider.d.ts +4 -3
- package/dist/sdk/actions/region-action-provider.js +6 -5
- package/dist/sdk/actions/region-action-provider.js.map +1 -1
- package/dist/sdk/actions/session-action-provider.d.ts +4 -3
- package/dist/sdk/actions/session-action-provider.js +6 -5
- package/dist/sdk/actions/session-action-provider.js.map +1 -1
- package/dist/sdk/actions/shipping-action-provider.d.ts +4 -3
- package/dist/sdk/actions/shipping-action-provider.js +6 -5
- package/dist/sdk/actions/shipping-action-provider.js.map +1 -1
- package/dist/sdk/cart/use-cart-actions.js.map +1 -1
- package/dist/sdk/client/create-client.js +24 -2
- package/dist/sdk/client/create-client.js.map +1 -1
- package/dist/sdk/content/create-content-runtime.js +1 -1
- package/dist/utils/safe-href.js +107 -1
- package/dist/utils/safe-href.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sign-in-form.js","names":[],"sources":["../../../src/matter/sign-in-form/sign-in-form.tsx"],"sourcesContent":["'use client';\n\nimport { defineComponent } from '@vtex/fs-core';\nimport type { FormEvent } from 'react';\nimport { useEffect, useState } from 'react';\nimport { Button } from '../../particles/button/button.js';\nimport { EmailField, isValidEmail } from '../../particles/email-field/email-field.js';\nimport { PasswordField } from '../../particles/password-field/password-field.js';\nimport type { ComponentActions } from '../../sdk/actions/types.js';\nimport { useDataOverride } from '../../sdk/data/data-override-context.js';\nimport type { SessionUserError } from '../../sdk/session/auth-operations.js';\nimport { useSession } from '../session/session.js';\n\n// ---------------------------------------------------------------------------\n// GraphQL Fragment (colocated, ADR-0011) — reads only clientProfile.email, the minimal slice this\n// matter owns for the pre-fill. Authored as a raw string (not the codegen graphql() factory),\n// mirroring EmailIdentificationControl; the compiler derives the entity fragment on `clientProfile`\n// and subscribes reactively.\n// ---------------------------------------------------------------------------\n\nconst fragment = /* GraphQL */ `\n fragment SignInFormData on Query {\n clientProfile {\n __typename\n id\n email\n }\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** ClientProfile slice forwarded by the reactive runtime — only `email` is consumed for pre-fill. */\nexport type SignInFormSlice = Readonly<{ email?: string | null }>;\n\nexport type SignInFormMessages = Readonly<{\n invalidCredentialsMessage: string;\n blockedMessage: string;\n networkErrorMessage: string;\n genericErrorMessage: string;\n}>;\n\nexport type SignInFormProps = Readonly<{\n emailLabel?: string;\n emailPlaceholder?: string;\n passwordLabel?: string;\n passwordPlaceholder?: string;\n showPasswordLabel?: string;\n hidePasswordLabel?: string;\n submitLabel?: string;\n /** Inline error for an invalid email format (mirrors EmailIdentificationControl). */\n invalidEmailMessage?: string;\n /** Inline error when the password field is submitted empty (this form owns the presence check). */\n passwordRequiredMessage?: string;\n invalidCredentialsMessage?: string;\n blockedMessage?: string;\n networkErrorMessage?: string;\n genericErrorMessage?: string;\n /** ClientProfile slice forwarded by the reactive runtime; `null` until the entity resolves. */\n $data?: SignInFormSlice | null;\n /** Compiler-injected action surface; carries `session.loginWithPassword`. */\n $actions?: ComponentActions;\n className?: string;\n}>;\n\n// ---------------------------------------------------------------------------\n// Error mapping — pure, unit-tested.\n// ---------------------------------------------------------------------------\n\n/**\n * Maps the first resolved SessionUserError to caller-configurable copy. INVALID_ACCESS_CODE,\n * ACCESS_CODE_SEND_LIMIT, and MISSING_AUTH_CONTEXT are contractually unreachable from\n * loginWithPassword but share the untyped `code: string` field, so they fall through to the\n * generic message defensively, alongside any future/unknown code.\n */\nexport function mapSessionErrorMessage(\n error: SessionUserError | undefined,\n messages: SignInFormMessages,\n): string | null {\n if (!error) return null;\n switch (error.code) {\n case 'INVALID_CREDENTIALS':\n return messages.invalidCredentialsMessage;\n case 'BLOCKED':\n return messages.blockedMessage;\n case 'NETWORK_ERROR':\n return messages.networkErrorMessage;\n default:\n return messages.genericErrorMessage;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Definition\n// ---------------------------------------------------------------------------\n\nexport const SignInFormDefinition = defineComponent<SignInFormProps>({\n name: 'SignInForm',\n importPath: '@vtex/faststore/components',\n component: SignInForm,\n fragment,\n fetchStrategy: 'reactive',\n actions: { session: ['loginWithPassword'] },\n requiresContext: 'session',\n editor: { label: 'Sign In Form', category: 'identity' },\n schema: {\n emailLabel: { type: 'string', default: 'Email', label: 'Email label' },\n emailPlaceholder: { type: 'string', default: 'you@example.com', label: 'Email placeholder' },\n passwordLabel: { type: 'string', default: 'Password', label: 'Password label' },\n passwordPlaceholder: { type: 'string', default: '', label: 'Password placeholder' },\n showPasswordLabel: { type: 'string', default: 'Show password', label: 'Show password label' },\n hidePasswordLabel: { type: 'string', default: 'Hide password', label: 'Hide password label' },\n submitLabel: { type: 'string', default: 'Sign In', label: 'Submit label' },\n invalidEmailMessage: { type: 'string', default: 'Enter a valid email address.', label: 'Invalid email message' },\n passwordRequiredMessage: {\n type: 'string',\n default: 'Enter your password.',\n label: 'Password required message',\n },\n invalidCredentialsMessage: {\n type: 'string',\n default: 'Incorrect email or password. Please try again.',\n label: 'Invalid credentials message',\n },\n blockedMessage: {\n type: 'string',\n default: 'This account has been blocked. Please contact support.',\n label: 'Blocked account message',\n },\n networkErrorMessage: {\n type: 'string',\n default: 'Unable to complete the request. Please try again.',\n label: 'Network error message',\n },\n genericErrorMessage: {\n type: 'string',\n default: 'Unable to sign in. Please try again.',\n label: 'Generic error message',\n },\n },\n guideline:\n 'Combined email+password sign-in form dispatching $actions.session.loginWithPassword; pre-fills the email from clientProfile.email, self-hides once the session is loggedIn, and exposes data-fs-sign-in-form-pending while the submit is in flight. Must be inside SessionRoot. See sign-in-form.md.',\n});\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Matter that composes `EmailField` + `PasswordField` under one submit and dispatches\n * `$actions.session.loginWithPassword`. Reads the email pre-fill from `clientProfile.email`\n * (colocated reactive fragment) and the identity tri-state from `useSession()` (which also\n * enforces the `SessionRoot` ancestor), self-gating to `null` once the shopper is `loggedIn`.\n * Client-side validation (email format, password presence) blocks the dispatch before any network\n * round trip; the awaited dispatch's `userErrors` map to one form-level inline message, distinct\n * from any ancestor `UNAUTHORIZED` gate. While the write is in flight the form is disabled and\n * `data-fs-sign-in-form-pending` is present so a checkout-CTA guard can block.\n */\nexport function SignInForm({\n emailLabel,\n emailPlaceholder,\n passwordLabel,\n passwordPlaceholder,\n showPasswordLabel,\n hidePasswordLabel,\n submitLabel = 'Sign In',\n invalidEmailMessage = 'Enter a valid email address.',\n passwordRequiredMessage = 'Enter your password.',\n invalidCredentialsMessage = 'Incorrect email or password. Please try again.',\n blockedMessage = 'This account has been blocked. Please contact support.',\n networkErrorMessage = 'Unable to complete the request. Please try again.',\n genericErrorMessage = 'Unable to sign in. Please try again.',\n $data: $dataProp,\n $actions,\n className,\n}: SignInFormProps) {\n const $data = useDataOverride($dataProp);\n // Reads the tri-state for self-gating and, as a side effect, throws when rendered outside\n // <SessionRoot> — called unconditionally, before any early return.\n const { state } = useSession();\n\n const savedEmail = $data?.email ?? '';\n const [email, setEmail] = useState(savedEmail);\n const [password, setPassword] = useState('');\n const [pending, setPending] = useState(false);\n const [emailError, setEmailError] = useState<string | null>(null);\n const [passwordError, setPasswordError] = useState<string | null>(null);\n const [formError, setFormError] = useState<string | null>(null);\n\n // Sync the pre-fill when the saved profile email resolves or changes, mirroring\n // EmailIdentificationControl's pre-fill sync.\n useEffect(() => {\n setEmail(savedEmail);\n }, [savedEmail]);\n\n // An already-authenticated shopper is never offered a sign-in form.\n if (state === 'loggedIn') return null;\n\n function handleEmailChange(next: string) {\n setEmail(next);\n if (emailError) setEmailError(null);\n }\n\n function handlePasswordChange(next: string) {\n setPassword(next);\n if (passwordError) setPasswordError(null);\n }\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n\n const trimmedEmail = email.trim();\n if (!isValidEmail(trimmedEmail)) {\n setEmailError(invalidEmailMessage);\n return;\n }\n\n if (password.length === 0) {\n setPasswordError(passwordRequiredMessage);\n return;\n }\n\n // Client-side checks passed — clear any prior field-level/form-level errors before dispatching.\n setEmailError(null);\n setPasswordError(null);\n setFormError(null);\n setPending(true);\n\n try {\n // Awaited (async/pessimistic): the cache flip is read on the next render via useSession(),\n // never assumed here.\n const errors = (await $actions?.session?.loginWithPassword?.({ email: trimmedEmail, password })) ?? [];\n\n if (errors.length > 0) {\n setFormError(\n mapSessionErrorMessage(errors[0], {\n invalidCredentialsMessage,\n blockedMessage,\n networkErrorMessage,\n genericErrorMessage,\n }),\n );\n // Credential hygiene: drop the rejected password, keep the email.\n setPassword('');\n }\n } finally {\n setPending(false);\n }\n }\n\n return (\n <form\n className={className}\n data-fs-sign-in-form=\"\"\n {...(pending ? { 'data-fs-sign-in-form-pending': '' } : {})}\n onSubmit={handleSubmit}\n >\n <EmailField\n label={emailLabel}\n placeholder={emailPlaceholder}\n value={email}\n disabled={pending}\n invalid={emailError != null}\n errorMessage={emailError ?? undefined}\n onValueChange={handleEmailChange}\n required\n />\n <PasswordField\n label={passwordLabel}\n placeholder={passwordPlaceholder}\n value={password}\n disabled={pending}\n invalid={passwordError != null}\n errorMessage={passwordError ?? undefined}\n showPasswordLabel={showPasswordLabel}\n hidePasswordLabel={hidePasswordLabel}\n onValueChange={handlePasswordChange}\n />\n {formError ? (\n <div data-fs-sign-in-form-error=\"\" role=\"alert\">\n {formError}\n </div>\n ) : null}\n <Button type=\"submit\" disabled={pending} data-fs-sign-in-form-submit=\"\">\n {submitLabel}\n </Button>\n </form>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sign-in-form.js","names":[],"sources":["../../../src/matter/sign-in-form/sign-in-form.tsx"],"sourcesContent":["'use client';\n\nimport { defineComponent } from '@vtex/fs-core';\nimport type { FormEvent } from 'react';\nimport { useEffect, useState } from 'react';\nimport { Button } from '../../particles/button/button.js';\nimport { EmailField, isValidEmail } from '../../particles/email-field/email-field.js';\nimport { PasswordField } from '../../particles/password-field/password-field.js';\nimport { useActionsOverride } from '../../sdk/actions/actions-override-context.js';\nimport type { ComponentActions } from '../../sdk/actions/types.js';\nimport { useDataOverride } from '../../sdk/data/data-override-context.js';\nimport type { SessionUserError } from '../../sdk/session/auth-operations.js';\nimport { useSession } from '../session/session.js';\n\n// ---------------------------------------------------------------------------\n// GraphQL Fragment (colocated, ADR-0011) — reads only clientProfile.email, the minimal slice this\n// matter owns for the pre-fill. Authored as a raw string (not the codegen graphql() factory),\n// mirroring EmailIdentificationControl; the compiler derives the entity fragment on `clientProfile`\n// and subscribes reactively.\n// ---------------------------------------------------------------------------\n\nconst fragment = /* GraphQL */ `\n fragment SignInFormData on Query {\n clientProfile {\n __typename\n id\n email\n }\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** ClientProfile slice forwarded by the reactive runtime — only `email` is consumed for pre-fill. */\nexport type SignInFormSlice = Readonly<{ email?: string | null }>;\n\nexport type SignInFormMessages = Readonly<{\n invalidCredentialsMessage: string;\n blockedMessage: string;\n networkErrorMessage: string;\n genericErrorMessage: string;\n}>;\n\nexport type SignInFormProps = Readonly<{\n emailLabel?: string;\n emailPlaceholder?: string;\n passwordLabel?: string;\n passwordPlaceholder?: string;\n showPasswordLabel?: string;\n hidePasswordLabel?: string;\n submitLabel?: string;\n /** Inline error for an invalid email format (mirrors EmailIdentificationControl). */\n invalidEmailMessage?: string;\n /** Inline error when the password field is submitted empty (this form owns the presence check). */\n passwordRequiredMessage?: string;\n invalidCredentialsMessage?: string;\n blockedMessage?: string;\n networkErrorMessage?: string;\n genericErrorMessage?: string;\n /** ClientProfile slice forwarded by the reactive runtime; `null` until the entity resolves. */\n $data?: SignInFormSlice | null;\n /** Compiler-injected action surface; carries `session.loginWithPassword`. */\n $actions?: ComponentActions;\n className?: string;\n}>;\n\n// ---------------------------------------------------------------------------\n// Error mapping — pure, unit-tested.\n// ---------------------------------------------------------------------------\n\n/**\n * Maps the first resolved SessionUserError to caller-configurable copy. INVALID_ACCESS_CODE,\n * ACCESS_CODE_SEND_LIMIT, and MISSING_AUTH_CONTEXT are contractually unreachable from\n * loginWithPassword but share the untyped `code: string` field, so they fall through to the\n * generic message defensively, alongside any future/unknown code.\n */\nexport function mapSessionErrorMessage(\n error: SessionUserError | undefined,\n messages: SignInFormMessages,\n): string | null {\n if (!error) return null;\n switch (error.code) {\n case 'INVALID_CREDENTIALS':\n return messages.invalidCredentialsMessage;\n case 'BLOCKED':\n return messages.blockedMessage;\n case 'NETWORK_ERROR':\n return messages.networkErrorMessage;\n default:\n return messages.genericErrorMessage;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Definition\n// ---------------------------------------------------------------------------\n\nexport const SignInFormDefinition = defineComponent<SignInFormProps>({\n name: 'SignInForm',\n importPath: '@vtex/faststore/components',\n component: SignInForm,\n fragment,\n fetchStrategy: 'reactive',\n actions: { session: ['loginWithPassword'] },\n requiresContext: 'session',\n editor: { label: 'Sign In Form', category: 'identity' },\n schema: {\n emailLabel: { type: 'string', default: 'Email', label: 'Email label' },\n emailPlaceholder: { type: 'string', default: 'you@example.com', label: 'Email placeholder' },\n passwordLabel: { type: 'string', default: 'Password', label: 'Password label' },\n passwordPlaceholder: { type: 'string', default: '', label: 'Password placeholder' },\n showPasswordLabel: { type: 'string', default: 'Show password', label: 'Show password label' },\n hidePasswordLabel: { type: 'string', default: 'Hide password', label: 'Hide password label' },\n submitLabel: { type: 'string', default: 'Sign In', label: 'Submit label' },\n invalidEmailMessage: { type: 'string', default: 'Enter a valid email address.', label: 'Invalid email message' },\n passwordRequiredMessage: {\n type: 'string',\n default: 'Enter your password.',\n label: 'Password required message',\n },\n invalidCredentialsMessage: {\n type: 'string',\n default: 'Incorrect email or password. Please try again.',\n label: 'Invalid credentials message',\n },\n blockedMessage: {\n type: 'string',\n default: 'This account has been blocked. Please contact support.',\n label: 'Blocked account message',\n },\n networkErrorMessage: {\n type: 'string',\n default: 'Unable to complete the request. Please try again.',\n label: 'Network error message',\n },\n genericErrorMessage: {\n type: 'string',\n default: 'Unable to sign in. Please try again.',\n label: 'Generic error message',\n },\n },\n guideline:\n 'Combined email+password sign-in form dispatching $actions.session.loginWithPassword; pre-fills the email from clientProfile.email, self-hides once the session is loggedIn, and exposes data-fs-sign-in-form-pending while the submit is in flight. Must be inside SessionRoot. See sign-in-form.md.',\n});\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/**\n * Matter that composes `EmailField` + `PasswordField` under one submit and dispatches\n * `$actions.session.loginWithPassword`. Reads the email pre-fill from `clientProfile.email`\n * (colocated reactive fragment) and the identity tri-state from `useSession()` (which also\n * enforces the `SessionRoot` ancestor), self-gating to `null` once the shopper is `loggedIn`.\n * Client-side validation (email format, password presence) blocks the dispatch before any network\n * round trip; the awaited dispatch's `userErrors` map to one form-level inline message, distinct\n * from any ancestor `UNAUTHORIZED` gate. While the write is in flight the form is disabled and\n * `data-fs-sign-in-form-pending` is present so a checkout-CTA guard can block.\n */\nexport function SignInForm({\n emailLabel,\n emailPlaceholder,\n passwordLabel,\n passwordPlaceholder,\n showPasswordLabel,\n hidePasswordLabel,\n submitLabel = 'Sign In',\n invalidEmailMessage = 'Enter a valid email address.',\n passwordRequiredMessage = 'Enter your password.',\n invalidCredentialsMessage = 'Incorrect email or password. Please try again.',\n blockedMessage = 'This account has been blocked. Please contact support.',\n networkErrorMessage = 'Unable to complete the request. Please try again.',\n genericErrorMessage = 'Unable to sign in. Please try again.',\n $data: $dataProp,\n $actions: $actionsProp,\n className,\n}: SignInFormProps) {\n const $data = useDataOverride($dataProp);\n const $actions = useActionsOverride($actionsProp);\n // Reads the tri-state for self-gating and, as a side effect, throws when rendered outside\n // <SessionRoot> — called unconditionally, before any early return.\n const { state } = useSession();\n\n const savedEmail = $data?.email ?? '';\n const [email, setEmail] = useState(savedEmail);\n const [password, setPassword] = useState('');\n const [pending, setPending] = useState(false);\n const [emailError, setEmailError] = useState<string | null>(null);\n const [passwordError, setPasswordError] = useState<string | null>(null);\n const [formError, setFormError] = useState<string | null>(null);\n\n // Sync the pre-fill when the saved profile email resolves or changes, mirroring\n // EmailIdentificationControl's pre-fill sync.\n useEffect(() => {\n setEmail(savedEmail);\n }, [savedEmail]);\n\n // An already-authenticated shopper is never offered a sign-in form.\n if (state === 'loggedIn') return null;\n\n function handleEmailChange(next: string) {\n setEmail(next);\n if (emailError) setEmailError(null);\n }\n\n function handlePasswordChange(next: string) {\n setPassword(next);\n if (passwordError) setPasswordError(null);\n }\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n\n const trimmedEmail = email.trim();\n if (!isValidEmail(trimmedEmail)) {\n setEmailError(invalidEmailMessage);\n return;\n }\n\n if (password.length === 0) {\n setPasswordError(passwordRequiredMessage);\n return;\n }\n\n // Client-side checks passed — clear any prior field-level/form-level errors before dispatching.\n setEmailError(null);\n setPasswordError(null);\n setFormError(null);\n setPending(true);\n\n try {\n // Awaited (async/pessimistic): the cache flip is read on the next render via useSession(),\n // never assumed here.\n const errors = (await $actions?.session?.loginWithPassword?.({ email: trimmedEmail, password })) ?? [];\n\n if (errors.length > 0) {\n setFormError(\n mapSessionErrorMessage(errors[0], {\n invalidCredentialsMessage,\n blockedMessage,\n networkErrorMessage,\n genericErrorMessage,\n }),\n );\n // Credential hygiene: drop the rejected password, keep the email.\n setPassword('');\n }\n } finally {\n setPending(false);\n }\n }\n\n return (\n <form\n className={className}\n data-fs-sign-in-form=\"\"\n {...(pending ? { 'data-fs-sign-in-form-pending': '' } : {})}\n onSubmit={handleSubmit}\n >\n <EmailField\n label={emailLabel}\n placeholder={emailPlaceholder}\n value={email}\n disabled={pending}\n invalid={emailError != null}\n errorMessage={emailError ?? undefined}\n onValueChange={handleEmailChange}\n required\n />\n <PasswordField\n label={passwordLabel}\n placeholder={passwordPlaceholder}\n value={password}\n disabled={pending}\n invalid={passwordError != null}\n errorMessage={passwordError ?? undefined}\n showPasswordLabel={showPasswordLabel}\n hidePasswordLabel={hidePasswordLabel}\n onValueChange={handlePasswordChange}\n />\n {formError ? (\n <div data-fs-sign-in-form-error=\"\" role=\"alert\">\n {formError}\n </div>\n ) : null}\n <Button type=\"submit\" disabled={pending} data-fs-sign-in-form-submit=\"\">\n {submitLabel}\n </Button>\n </form>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ContentValue } from "../../core/dist/define-content-type.js";
|
|
2
|
+
import { ComponentDefinition } from "../../core/dist/define-component.js";
|
|
3
|
+
import "../../core/dist/index.js";
|
|
4
|
+
import { ComponentPropsWithRef } from "react";
|
|
5
|
+
//#region src/matter/sign-in-redirect/sign-in-redirect.d.ts
|
|
6
|
+
export declare const SignInRedirectContent: {
|
|
7
|
+
readonly id: "sign-in-redirect";
|
|
8
|
+
readonly title: "Sign In Redirect";
|
|
9
|
+
readonly description: "Waiting message shown while the shopper is handed over to the platform login.";
|
|
10
|
+
readonly schema: {
|
|
11
|
+
readonly type: "object";
|
|
12
|
+
readonly additionalProperties: false;
|
|
13
|
+
readonly properties: {
|
|
14
|
+
readonly loadingLabel: {
|
|
15
|
+
readonly type: "string";
|
|
16
|
+
readonly minLength: 1;
|
|
17
|
+
readonly title: "Waiting message";
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/** Props for {@link SignInRedirect}. Extends native `<output>` attributes (the rendered root). */
|
|
23
|
+
export type SignInRedirectProps = Omit<ComponentPropsWithRef<'output'>, 'children'> & {
|
|
24
|
+
/** The platform login address, per account. Absolute URL or site-relative path; both are spellings
|
|
25
|
+
* example stores use. Optional in the type only because schema props are; the render gate requires
|
|
26
|
+
* a resolved value. */
|
|
27
|
+
platformLoginUrl?: string;
|
|
28
|
+
/** Text shown while the handover is pending. */
|
|
29
|
+
loadingLabel?: string;
|
|
30
|
+
/** Merchant-authored waiting message, sliced from the page content entry; wins over the prop when
|
|
31
|
+
* present. Absent in a non-CMS store — the prop is the fallback. */
|
|
32
|
+
$content?: ContentValue<typeof SignInRedirectContent>;
|
|
33
|
+
};
|
|
34
|
+
/** Props for {@link SignInRedirectLoading}. Extends native `<output>` attributes. */
|
|
35
|
+
export type SignInRedirectLoadingProps = ComponentPropsWithRef<'output'>;
|
|
36
|
+
export declare const SignInRedirectDefinition: ComponentDefinition<SignInRedirectProps>;
|
|
37
|
+
export declare function SignInRedirect({ platformLoginUrl, loadingLabel, $content, className, ref, ...props }: SignInRedirectProps): import("react/jsx-runtime").JSX.Element | null;
|
|
38
|
+
/**
|
|
39
|
+
* The waiting look, and the page's only content while the handover is pending. A real part with its own
|
|
40
|
+
* hook rather than an ad hoc spinner, so a store can style it and a story can render it — mounting the
|
|
41
|
+
* component itself would navigate the frame away.
|
|
42
|
+
*/
|
|
43
|
+
export declare function SignInRedirectLoading({ className, children, ...props }: SignInRedirectLoadingProps): import("react/jsx-runtime").JSX.Element;
|
|
44
|
+
//#endregion
|
|
45
|
+
//# sourceMappingURL=sign-in-redirect.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { defineComponent } from "../../core/dist/define-component.js";
|
|
3
|
+
import { defineContentType } from "../../core/dist/define-content-type.js";
|
|
4
|
+
import "../../core/dist/index.js";
|
|
5
|
+
import { RETURN_URL_PARAM, isSignInRoute, resolveLoginAddressHref, resolveReturnHref } from "../../utils/safe-href.js";
|
|
6
|
+
import { useEffect, useRef } from "react";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
//#region src/matter/sign-in-redirect/sign-in-redirect.tsx
|
|
9
|
+
const SignInRedirectContent = defineContentType({
|
|
10
|
+
id: "sign-in-redirect",
|
|
11
|
+
title: "Sign In Redirect",
|
|
12
|
+
description: "Waiting message shown while the shopper is handed over to the platform login.",
|
|
13
|
+
schema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
properties: { loadingLabel: {
|
|
17
|
+
type: "string",
|
|
18
|
+
minLength: 1,
|
|
19
|
+
title: "Waiting message"
|
|
20
|
+
} }
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const SignInRedirectDefinition = defineComponent({
|
|
24
|
+
name: "SignInRedirect",
|
|
25
|
+
importPath: "@vtex/faststore/components",
|
|
26
|
+
component: SignInRedirect,
|
|
27
|
+
content: SignInRedirectContent,
|
|
28
|
+
editor: {
|
|
29
|
+
label: "Sign In Redirect",
|
|
30
|
+
category: "identity"
|
|
31
|
+
},
|
|
32
|
+
schema: {
|
|
33
|
+
platformLoginUrl: {
|
|
34
|
+
type: "string",
|
|
35
|
+
label: "Platform Login URL"
|
|
36
|
+
},
|
|
37
|
+
loadingLabel: {
|
|
38
|
+
type: "string",
|
|
39
|
+
label: "Waiting Message"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
guideline: "The whole of the /login route: on mount it replaces the document with the account's platform login, forwarding a validated returnUrl from the incoming query. It is a handover, not a block — place it alone on its own page, never inside a header, drawer, or any page a shopper is meant to read, because anything composed beside it is navigated away from before it can be seen. Never place it under an account shell or behind AccountAccessGate, whose redirect targets this route. It renders no sign-in fields of its own and collects no credentials — the shopper types them on the platform, after this component has already left the page. platformLoginUrl is required and per-account (absolute URL or site-relative path), and must not be the storefront's own /login, which would hand this route to itself — with no usable value the component renders nothing and warns."
|
|
43
|
+
});
|
|
44
|
+
function SignInRedirect({ platformLoginUrl, loadingLabel, $content, className, ref, ...props }) {
|
|
45
|
+
const handedOverRef = useRef(false);
|
|
46
|
+
const warnedForRef = useRef(null);
|
|
47
|
+
const entryLoadingLabel = $content?.loadingLabel?.trim() ? $content.loadingLabel : loadingLabel;
|
|
48
|
+
const address = platformLoginUrl?.trim() ? resolveLoginAddressHref(platformLoginUrl) : null;
|
|
49
|
+
const loginHref = address?.safe ? address.href : null;
|
|
50
|
+
const rejection = address !== null && !address.safe ? address.reason : null;
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (loginHref !== null) {
|
|
53
|
+
if (handedOverRef.current) return;
|
|
54
|
+
const handover = composeHandover(loginHref);
|
|
55
|
+
if (handover === null) {
|
|
56
|
+
if (warnedForRef.current !== "sign-in-route") {
|
|
57
|
+
warnedForRef.current = "sign-in-route";
|
|
58
|
+
console.warn("[SignInRedirect] platform login address rejected", { reason: "sign-in-route" });
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
handedOverRef.current = true;
|
|
63
|
+
globalThis.location.replace(handover);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const problem = rejection ?? "unauthored";
|
|
67
|
+
if (warnedForRef.current === problem) return;
|
|
68
|
+
warnedForRef.current = problem;
|
|
69
|
+
if (rejection !== null) {
|
|
70
|
+
console.warn("[SignInRedirect] platform login address rejected", { reason: rejection });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.warn("[SignInRedirect] no platform login address authored");
|
|
74
|
+
}, [loginHref, rejection]);
|
|
75
|
+
if (loginHref === null) return null;
|
|
76
|
+
return /* @__PURE__ */ jsx(SignInRedirectLoading, {
|
|
77
|
+
...props,
|
|
78
|
+
ref,
|
|
79
|
+
className,
|
|
80
|
+
"data-fs-sign-in-redirect": "",
|
|
81
|
+
children: entryLoadingLabel
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Builds the address the document is handed over to: the authored one, carrying the shopper's return
|
|
86
|
+
* when the arriving value survives revalidation.
|
|
87
|
+
*
|
|
88
|
+
* Revalidating here rather than trusting the sender is the boundary — `/login` is reachable by a
|
|
89
|
+
* hand-crafted link, so the value is shopper-supplied whoever produced it. Only this one parameter
|
|
90
|
+
* crosses; anything else on the incoming URL is dropped rather than forwarded into an auth URL.
|
|
91
|
+
*
|
|
92
|
+
* `null` when the address resolves back to this route, which is a handover that would reload the same
|
|
93
|
+
* document and mount the same handover again. The site-relative spelling of that is already refused
|
|
94
|
+
* while rendering; the absolute one needs the document's own origin, which exists only here.
|
|
95
|
+
*/
|
|
96
|
+
function composeHandover(loginHref) {
|
|
97
|
+
const destination = new URL(loginHref, globalThis.location.origin);
|
|
98
|
+
if (destination.origin === globalThis.location.origin && isSignInRoute(destination.pathname)) return null;
|
|
99
|
+
const arriving = new URLSearchParams(globalThis.location.search).get(RETURN_URL_PARAM);
|
|
100
|
+
if (arriving === null) return destination.toString();
|
|
101
|
+
const returnHref = resolveReturnHref(arriving);
|
|
102
|
+
if (!returnHref.safe) {
|
|
103
|
+
console.warn("[SignInRedirect] return destination rejected", { reason: returnHref.reason });
|
|
104
|
+
return destination.toString();
|
|
105
|
+
}
|
|
106
|
+
const value = destination.origin === globalThis.location.origin ? returnHref.href : new URL(returnHref.href, globalThis.location.origin).toString();
|
|
107
|
+
destination.searchParams.set(RETURN_URL_PARAM, value);
|
|
108
|
+
return destination.toString();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The waiting look, and the page's only content while the handover is pending. A real part with its own
|
|
112
|
+
* hook rather than an ad hoc spinner, so a store can style it and a story can render it — mounting the
|
|
113
|
+
* component itself would navigate the frame away.
|
|
114
|
+
*/
|
|
115
|
+
function SignInRedirectLoading({ className, children, ...props }) {
|
|
116
|
+
return /* @__PURE__ */ jsx("output", {
|
|
117
|
+
...props,
|
|
118
|
+
"aria-live": "polite",
|
|
119
|
+
className,
|
|
120
|
+
"data-fs-sign-in-redirect-loading": "",
|
|
121
|
+
children
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
export { SignInRedirect, SignInRedirectContent, SignInRedirectDefinition, SignInRedirectLoading };
|
|
126
|
+
|
|
127
|
+
//# sourceMappingURL=sign-in-redirect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign-in-redirect.js","names":[],"sources":["../../../src/matter/sign-in-redirect/sign-in-redirect.tsx"],"sourcesContent":["'use client';\n\nimport { type ContentValue, defineComponent, defineContentType } from '@vtex/fs-core';\nimport { type ComponentPropsWithRef, useEffect, useRef } from 'react';\nimport { isSignInRoute, RETURN_URL_PARAM, resolveLoginAddressHref, resolveReturnHref } from '../../utils/safe-href.js';\n\n// Nothing is required, following the shipped entry point: an entry may author any subset, and a slice\n// missing a required property is shape-invalid, which drops the whole element rather than the one field.\n// The address is deliberately absent — schema is developer-owned and lands through review, while an\n// entry is editable at runtime, and the runtime channel is the one that could misroute an auth flow.\nexport const SignInRedirectContent = defineContentType({\n id: 'sign-in-redirect',\n title: 'Sign In Redirect',\n description: 'Waiting message shown while the shopper is handed over to the platform login.',\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n loadingLabel: { type: 'string', minLength: 1, title: 'Waiting message' },\n },\n },\n});\n\n/** Props for {@link SignInRedirect}. Extends native `<output>` attributes (the rendered root). */\nexport type SignInRedirectProps = Omit<ComponentPropsWithRef<'output'>, 'children'> & {\n /** The platform login address, per account. Absolute URL or site-relative path; both are spellings\n * example stores use. Optional in the type only because schema props are; the render gate requires\n * a resolved value. */\n platformLoginUrl?: string;\n /** Text shown while the handover is pending. */\n loadingLabel?: string;\n /** Merchant-authored waiting message, sliced from the page content entry; wins over the prop when\n * present. Absent in a non-CMS store — the prop is the fallback. */\n $content?: ContentValue<typeof SignInRedirectContent>;\n};\n\n/** Props for {@link SignInRedirectLoading}. Extends native `<output>` attributes. */\nexport type SignInRedirectLoadingProps = ComponentPropsWithRef<'output'>;\n\nexport const SignInRedirectDefinition = defineComponent<SignInRedirectProps>({\n name: 'SignInRedirect',\n importPath: '@vtex/faststore/components',\n component: SignInRedirect,\n content: SignInRedirectContent,\n editor: { label: 'Sign In Redirect', category: 'identity' },\n schema: {\n platformLoginUrl: { type: 'string', label: 'Platform Login URL' },\n // Mirrored from the content model on purpose, as every content-bearing component in the library\n // does: the entry wins when published, and this is the value a store without a CMS authors.\n loadingLabel: { type: 'string', label: 'Waiting Message' },\n },\n guideline:\n \"The whole of the /login route: on mount it replaces the document with the account's platform login, forwarding a validated returnUrl from the incoming query. It is a handover, not a block — place it alone on its own page, never inside a header, drawer, or any page a shopper is meant to read, because anything composed beside it is navigated away from before it can be seen. Never place it under an account shell or behind AccountAccessGate, whose redirect targets this route. It renders no sign-in fields of its own and collects no credentials — the shopper types them on the platform, after this component has already left the page. platformLoginUrl is required and per-account (absolute URL or site-relative path), and must not be the storefront's own /login, which would hand this route to itself — with no usable value the component renders nothing and warns.\",\n});\n\nexport function SignInRedirect({\n platformLoginUrl,\n loadingLabel,\n $content,\n className,\n ref,\n ...props\n}: SignInRedirectProps) {\n // StrictMode mounts an effect twice, and a second handover on a document already leaving is a race\n // with the navigation rather than a no-op.\n const handedOverRef = useRef(false);\n // The problem already reported. Warnings belong to the effect rather than the render body, and a\n // render is repeatable, so without this one unusable address would log on every pass.\n const warnedForRef = useRef<string | null>(null);\n\n // A blank entry field falls through, so clearing one in the Admin reads as unauthored.\n const entryLoadingLabel = $content?.loadingLabel?.trim() ? $content.loadingLabel : loadingLabel;\n\n // The only gate between an authored value and `location.replace`. A blank prop is the unauthored case\n // and reads as absent rather than as a refusal, so the two warn differently.\n const address = platformLoginUrl?.trim() ? resolveLoginAddressHref(platformLoginUrl) : null;\n const loginHref = address?.safe ? address.href : null;\n // Carried as the reason alone, so the effect depends on primitives and not on a per-render object.\n const rejection = address !== null && !address.safe ? address.reason : null;\n\n useEffect(() => {\n if (loginHref !== null) {\n if (handedOverRef.current) {\n return;\n }\n const handover = composeHandover(loginHref);\n // An address that resolves back to this very route, in the one spelling the render gate cannot\n // judge. Reported under the reason the gate would have used, so both spellings read alike.\n if (handover === null) {\n if (warnedForRef.current !== 'sign-in-route') {\n warnedForRef.current = 'sign-in-route';\n console.warn('[SignInRedirect] platform login address rejected', { reason: 'sign-in-route' });\n }\n return;\n }\n handedOverRef.current = true;\n globalThis.location.replace(handover);\n return;\n }\n\n // Keyed by the problem rather than raised once, so an address edited into a different unusable\n // value is still reported, and one that becomes usable still hands over above. `unauthored` names\n // the case with no rejection reason, and is outside the reason union it shares this slot with.\n const problem = rejection ?? 'unauthored';\n if (warnedForRef.current === problem) {\n return;\n }\n warnedForRef.current = problem;\n\n if (rejection !== null) {\n // A bounded reason and no authored value, the convention the content runtime's own log follows.\n console.warn('[SignInRedirect] platform login address rejected', { reason: rejection });\n return;\n }\n console.warn('[SignInRedirect] no platform login address authored');\n }, [loginHref, rejection]);\n\n // Both unusable cases render nothing; the effect above is what tells them apart in the log.\n if (loginHref === null) {\n return null;\n }\n\n // The waiting look is this component's only element, so it carries both hooks: its own, and the\n // component's identity, which a store targets by name the way every other component is targeted.\n // Rendered standalone the part keeps only its own, because standalone it is not the route.\n return (\n <SignInRedirectLoading {...props} ref={ref} className={className} data-fs-sign-in-redirect=\"\">\n {entryLoadingLabel}\n </SignInRedirectLoading>\n );\n}\n\n/**\n * Builds the address the document is handed over to: the authored one, carrying the shopper's return\n * when the arriving value survives revalidation.\n *\n * Revalidating here rather than trusting the sender is the boundary — `/login` is reachable by a\n * hand-crafted link, so the value is shopper-supplied whoever produced it. Only this one parameter\n * crosses; anything else on the incoming URL is dropped rather than forwarded into an auth URL.\n *\n * `null` when the address resolves back to this route, which is a handover that would reload the same\n * document and mount the same handover again. The site-relative spelling of that is already refused\n * while rendering; the absolute one needs the document's own origin, which exists only here.\n */\nfunction composeHandover(loginHref: string): string | null {\n // Accepts both authored spellings, resolving the relative one against the store's own origin.\n const destination = new URL(loginHref, globalThis.location.origin);\n\n // A cross-origin login page whose path happens to be `/login` is a different page, and travels.\n if (destination.origin === globalThis.location.origin && isSignInRoute(destination.pathname)) {\n return null;\n }\n\n // Read from the document rather than through `useSearchParams()`, which would require a `Suspense`\n // boundary the generator does not emit on a prerendered route.\n const arriving = new URLSearchParams(globalThis.location.search).get(RETURN_URL_PARAM);\n if (arriving === null) {\n return destination.toString();\n }\n\n const returnHref = resolveReturnHref(arriving);\n if (!returnHref.safe) {\n console.warn('[SignInRedirect] return destination rejected', { reason: returnHref.reason });\n return destination.toString();\n }\n\n // A path alone cannot say which origin it belongs to, and the store author is the one who decides\n // whether the receiver shares ours. A same-origin endpoint takes the path as it arrived, the way\n // legacy hands its own endpoint a bare `returnUrl`; a cross-origin one would resolve that path\n // against its own domain, so it is absolutized — always against the storefront, never the address.\n const sameOrigin = destination.origin === globalThis.location.origin;\n const value = sameOrigin ? returnHref.href : new URL(returnHref.href, globalThis.location.origin).toString();\n destination.searchParams.set(RETURN_URL_PARAM, value);\n\n return destination.toString();\n}\n\n/**\n * The waiting look, and the page's only content while the handover is pending. A real part with its own\n * hook rather than an ad hoc spinner, so a store can style it and a story can render it — mounting the\n * component itself would navigate the frame away.\n */\nexport function SignInRedirectLoading({ className, children, ...props }: SignInRedirectLoadingProps) {\n return (\n <output {...props} aria-live=\"polite\" className={className} data-fs-sign-in-redirect-loading=\"\">\n {children}\n </output>\n );\n}\n"],"mappings":";;;;;;;;AAUA,MAAa,wBAAwB,kBAAkB;CACrD,IAAI;CACJ,OAAO;CACP,aAAa;CACb,QAAQ;EACN,MAAM;EACN,sBAAsB;EACtB,YAAY,EACV,cAAc;GAAE,MAAM;GAAU,WAAW;GAAG,OAAO;EAAkB,EACzE;CACF;AACF,CAAC;AAkBD,MAAa,2BAA2B,gBAAqC;CAC3E,MAAM;CACN,YAAY;CACZ,WAAW;CACX,SAAS;CACT,QAAQ;EAAE,OAAO;EAAoB,UAAU;CAAW;CAC1D,QAAQ;EACN,kBAAkB;GAAE,MAAM;GAAU,OAAO;EAAqB;EAGhE,cAAc;GAAE,MAAM;GAAU,OAAO;EAAkB;CAC3D;CACA,WACE;AACJ,CAAC;AAED,SAAgB,eAAe,EAC7B,kBACA,cACA,UACA,WACA,KACA,GAAG,SACmB;CAGtB,MAAM,gBAAgB,OAAO,KAAK;CAGlC,MAAM,eAAe,OAAsB,IAAI;CAG/C,MAAM,oBAAoB,UAAU,cAAc,KAAK,IAAI,SAAS,eAAe;CAInF,MAAM,UAAU,kBAAkB,KAAK,IAAI,wBAAwB,gBAAgB,IAAI;CACvF,MAAM,YAAY,SAAS,OAAO,QAAQ,OAAO;CAEjD,MAAM,YAAY,YAAY,QAAQ,CAAC,QAAQ,OAAO,QAAQ,SAAS;CAEvE,gBAAgB;EACd,IAAI,cAAc,MAAM;GACtB,IAAI,cAAc,SAChB;GAEF,MAAM,WAAW,gBAAgB,SAAS;GAG1C,IAAI,aAAa,MAAM;IACrB,IAAI,aAAa,YAAY,iBAAiB;KAC5C,aAAa,UAAU;KACvB,QAAQ,KAAK,oDAAoD,EAAE,QAAQ,gBAAgB,CAAC;IAC9F;IACA;GACF;GACA,cAAc,UAAU;GACxB,WAAW,SAAS,QAAQ,QAAQ;GACpC;EACF;EAKA,MAAM,UAAU,aAAa;EAC7B,IAAI,aAAa,YAAY,SAC3B;EAEF,aAAa,UAAU;EAEvB,IAAI,cAAc,MAAM;GAEtB,QAAQ,KAAK,oDAAoD,EAAE,QAAQ,UAAU,CAAC;GACtF;EACF;EACA,QAAQ,KAAK,qDAAqD;CACpE,GAAG,CAAC,WAAW,SAAS,CAAC;CAGzB,IAAI,cAAc,MAChB,OAAO;CAMT,OACE,oBAAC,uBAAD;EAAuB,GAAI;EAAY;EAAgB;EAAW,4BAAyB;EACxF,UAAA;CACoB,CAAA;AAE3B;;;;;;;;;;;;;AAcA,SAAS,gBAAgB,WAAkC;CAEzD,MAAM,cAAc,IAAI,IAAI,WAAW,WAAW,SAAS,MAAM;CAGjE,IAAI,YAAY,WAAW,WAAW,SAAS,UAAU,cAAc,YAAY,QAAQ,GACzF,OAAO;CAKT,MAAM,WAAW,IAAI,gBAAgB,WAAW,SAAS,MAAM,CAAC,CAAC,IAAI,gBAAgB;CACrF,IAAI,aAAa,MACf,OAAO,YAAY,SAAS;CAG9B,MAAM,aAAa,kBAAkB,QAAQ;CAC7C,IAAI,CAAC,WAAW,MAAM;EACpB,QAAQ,KAAK,gDAAgD,EAAE,QAAQ,WAAW,OAAO,CAAC;EAC1F,OAAO,YAAY,SAAS;CAC9B;CAOA,MAAM,QADa,YAAY,WAAW,WAAW,SAAS,SACnC,WAAW,OAAO,IAAI,IAAI,WAAW,MAAM,WAAW,SAAS,MAAM,CAAC,CAAC,SAAS;CAC3G,YAAY,aAAa,IAAI,kBAAkB,KAAK;CAEpD,OAAO,YAAY,SAAS;AAC9B;;;;;;AAOA,SAAgB,sBAAsB,EAAE,WAAW,UAAU,GAAG,SAAqC;CACnG,OACE,oBAAC,UAAD;EAAQ,GAAI;EAAO,aAAU;EAAoB;EAAW,oCAAiC;EAC1F;CACK,CAAA;AAEZ"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
//#region src/sdk/actions/actions-override-context.ts
|
|
4
|
+
/**
|
|
5
|
+
* Context every action provider (`CartActionProvider`, `IdentityActionProvider`, ...) publishes to
|
|
6
|
+
* and every `$actions`-declaring component reads via `useActionsOverride`, replacing the
|
|
7
|
+
* `isValidElement`/`cloneElement` delivery that broke under React 19 RSC — children cross the flight
|
|
8
|
+
* boundary as `Symbol(react.lazy)` references that `isValidElement` rejects, so the clone silently
|
|
9
|
+
* never ran. Mirrors `DataOverrideContext`, the same fix applied to `$data` in 2026-05.
|
|
10
|
+
*/
|
|
11
|
+
const ActionsOverrideContext = createContext(void 0);
|
|
12
|
+
/**
|
|
13
|
+
* Merges `override`'s namespaces onto `base`, namespace by namespace, matching the merge
|
|
14
|
+
* `injectActions` already did via `{...inherited, [namespace]: selected}`: a namespace present in
|
|
15
|
+
* `override` replaces the same-named namespace in `base` wholesale; every other namespace in `base`
|
|
16
|
+
* is kept.
|
|
17
|
+
*/
|
|
18
|
+
function mergeComponentActions(base, override) {
|
|
19
|
+
return {
|
|
20
|
+
...base,
|
|
21
|
+
...override
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the effective `$actions` for a component that declares `actions: { ... }`: the ambient
|
|
26
|
+
* `ActionsOverrideContext` (published by however many providers the compiler nested around this
|
|
27
|
+
* element) merged over `$actionsProp`, so a namespace only the prop carries survives. With no
|
|
28
|
+
* provider mounted — the `mockAction`/story-authoring case — this is just `$actionsProp`.
|
|
29
|
+
*/
|
|
30
|
+
function useActionsOverride($actionsProp) {
|
|
31
|
+
const override = useContext(ActionsOverrideContext);
|
|
32
|
+
return override === void 0 ? $actionsProp : mergeComponentActions($actionsProp, override);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { ActionsOverrideContext, mergeComponentActions, useActionsOverride };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=actions-override-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions-override-context.js","names":[],"sources":["../../../src/sdk/actions/actions-override-context.ts"],"sourcesContent":["'use client';\n\nimport { createContext, useContext } from 'react';\nimport type { ComponentActions } from './types.js';\n\n/**\n * Context every action provider (`CartActionProvider`, `IdentityActionProvider`, ...) publishes to\n * and every `$actions`-declaring component reads via `useActionsOverride`, replacing the\n * `isValidElement`/`cloneElement` delivery that broke under React 19 RSC — children cross the flight\n * boundary as `Symbol(react.lazy)` references that `isValidElement` rejects, so the clone silently\n * never ran. Mirrors `DataOverrideContext`, the same fix applied to `$data` in 2026-05.\n */\nexport const ActionsOverrideContext = createContext<ComponentActions | undefined>(undefined);\n\n/**\n * Merges `override`'s namespaces onto `base`, namespace by namespace, matching the merge\n * `injectActions` already did via `{...inherited, [namespace]: selected}`: a namespace present in\n * `override` replaces the same-named namespace in `base` wholesale; every other namespace in `base`\n * is kept.\n */\nexport function mergeComponentActions(\n base: ComponentActions | undefined,\n override: ComponentActions,\n): ComponentActions {\n return { ...base, ...override };\n}\n\n/**\n * Resolves the effective `$actions` for a component that declares `actions: { ... }`: the ambient\n * `ActionsOverrideContext` (published by however many providers the compiler nested around this\n * element) merged over `$actionsProp`, so a namespace only the prop carries survives. With no\n * provider mounted — the `mockAction`/story-authoring case — this is just `$actionsProp`.\n */\nexport function useActionsOverride($actionsProp: ComponentActions | undefined): ComponentActions | undefined {\n const override = useContext(ActionsOverrideContext);\n return override === undefined ? $actionsProp : mergeComponentActions($actionsProp, override);\n}\n"],"mappings":";;;;;;;;;;AAYA,MAAa,yBAAyB,cAA4C,KAAA,CAAS;;;;;;;AAQ3F,SAAgB,sBACd,MACA,UACkB;CAClB,OAAO;EAAE,GAAG;EAAM,GAAG;CAAS;AAChC;;;;;;;AAQA,SAAgB,mBAAmB,cAA0E;CAC3G,MAAM,WAAW,WAAW,sBAAsB;CAClD,OAAO,aAAa,KAAA,IAAY,eAAe,sBAAsB,cAAc,QAAQ;AAC7F"}
|
|
@@ -6,13 +6,15 @@ export type CartActionProviderProps = ActionProviderProps;
|
|
|
6
6
|
* Compiler-emitted runtime wrapper for the `cart` action namespace — the write-side mirror of
|
|
7
7
|
* `ReactiveDataProvider`. The compiler wraps every element whose definition declares
|
|
8
8
|
* `actions: { cart: [...] }` with `<CartActionProvider methods={[...]}>`, calls `useCartActions`
|
|
9
|
-
* once, narrows the surface to the declared `methods`, and
|
|
10
|
-
*
|
|
11
|
-
* declare is never wrapped, so its hook is never
|
|
9
|
+
* once, narrows the surface to the declared `methods`, and publishes it through
|
|
10
|
+
* `ActionsOverrideContext` (merging with any outer namespace provider) for `useActionsOverride` to
|
|
11
|
+
* read further down. A namespace a component does not declare is never wrapped, so its hook is never
|
|
12
|
+
* called (specs/actions.md FR-6).
|
|
12
13
|
*
|
|
13
|
-
* It
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* It delivers `$actions` through context, not a `cloneElement` prop injection, matching `$data`
|
|
15
|
+
* (`DataOverrideContext`/`useDataOverride`) — see `useInjectActions`. A component with no provider
|
|
16
|
+
* above it (`mockAction` in a test or story) still works: `useActionsOverride` falls back to
|
|
17
|
+
* whatever `$actions` prop the component was directly given.
|
|
16
18
|
*/
|
|
17
19
|
export declare function CartActionProvider(props: CartActionProviderProps): ReactNode;
|
|
18
20
|
//#endregion
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useCartActions } from "../cart/use-cart-actions.js";
|
|
3
|
-
import {
|
|
3
|
+
import { useInjectActions } from "./inject-actions.js";
|
|
4
4
|
//#region src/sdk/actions/cart-action-provider.tsx
|
|
5
5
|
/**
|
|
6
6
|
* Compiler-emitted runtime wrapper for the `cart` action namespace — the write-side mirror of
|
|
7
7
|
* `ReactiveDataProvider`. The compiler wraps every element whose definition declares
|
|
8
8
|
* `actions: { cart: [...] }` with `<CartActionProvider methods={[...]}>`, calls `useCartActions`
|
|
9
|
-
* once, narrows the surface to the declared `methods`, and
|
|
10
|
-
*
|
|
11
|
-
* declare is never wrapped, so its hook is never
|
|
9
|
+
* once, narrows the surface to the declared `methods`, and publishes it through
|
|
10
|
+
* `ActionsOverrideContext` (merging with any outer namespace provider) for `useActionsOverride` to
|
|
11
|
+
* read further down. A namespace a component does not declare is never wrapped, so its hook is never
|
|
12
|
+
* called (specs/actions.md FR-6).
|
|
12
13
|
*
|
|
13
|
-
* It
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* It delivers `$actions` through context, not a `cloneElement` prop injection, matching `$data`
|
|
15
|
+
* (`DataOverrideContext`/`useDataOverride`) — see `useInjectActions`. A component with no provider
|
|
16
|
+
* above it (`mockAction` in a test or story) still works: `useActionsOverride` falls back to
|
|
17
|
+
* whatever `$actions` prop the component was directly given.
|
|
16
18
|
*/
|
|
17
19
|
function CartActionProvider(props) {
|
|
18
|
-
return
|
|
20
|
+
return useInjectActions("cart", useCartActions(), props);
|
|
19
21
|
}
|
|
20
22
|
//#endregion
|
|
21
23
|
export { CartActionProvider };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cart-action-provider.js","names":[],"sources":["../../../src/sdk/actions/cart-action-provider.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport { useCartActions } from '../cart/use-cart-actions.js';\nimport { type ActionProviderProps,
|
|
1
|
+
{"version":3,"file":"cart-action-provider.js","names":[],"sources":["../../../src/sdk/actions/cart-action-provider.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport { useCartActions } from '../cart/use-cart-actions.js';\nimport { type ActionProviderProps, useInjectActions } from './inject-actions.js';\n\nexport type CartActionProviderProps = ActionProviderProps;\n\n/**\n * Compiler-emitted runtime wrapper for the `cart` action namespace — the write-side mirror of\n * `ReactiveDataProvider`. The compiler wraps every element whose definition declares\n * `actions: { cart: [...] }` with `<CartActionProvider methods={[...]}>`, calls `useCartActions`\n * once, narrows the surface to the declared `methods`, and publishes it through\n * `ActionsOverrideContext` (merging with any outer namespace provider) for `useActionsOverride` to\n * read further down. A namespace a component does not declare is never wrapped, so its hook is never\n * called (specs/actions.md FR-6).\n *\n * It delivers `$actions` through context, not a `cloneElement` prop injection, matching `$data`\n * (`DataOverrideContext`/`useDataOverride`) — see `useInjectActions`. A component with no provider\n * above it (`mockAction` in a test or story) still works: `useActionsOverride` falls back to\n * whatever `$actions` prop the component was directly given.\n */\nexport function CartActionProvider(props: CartActionProviderProps): ReactNode {\n return useInjectActions('cart', useCartActions(), props);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAsBA,SAAgB,mBAAmB,OAA2C;CAC5E,OAAO,iBAAiB,QAAQ,eAAe,GAAG,KAAK;AACzD"}
|
|
@@ -7,8 +7,8 @@ export type IdentityActionProviderProps = ActionProviderProps;
|
|
|
7
7
|
* `ReactiveDataProvider`, structurally identical to `CartActionProvider`. The compiler wraps every
|
|
8
8
|
* element whose definition declares `actions: { identity: [...] }` with
|
|
9
9
|
* `<IdentityActionProvider methods={[...]}>`, calls `useIdentityActions` once, narrows the surface to
|
|
10
|
-
* the declared `methods`, and
|
|
11
|
-
*
|
|
10
|
+
* the declared `methods`, and publishes it through `ActionsOverrideContext` (merging with any outer
|
|
11
|
+
* namespace provider) for `useActionsOverride` to read further down.
|
|
12
12
|
*/
|
|
13
13
|
export declare function IdentityActionProvider(props: IdentityActionProviderProps): ReactNode;
|
|
14
14
|
//#endregion
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { useInjectActions } from "./inject-actions.js";
|
|
3
3
|
import { useIdentityActions } from "../identity/use-identity-actions.js";
|
|
4
4
|
//#region src/sdk/actions/identity-action-provider.tsx
|
|
5
5
|
/**
|
|
@@ -7,11 +7,11 @@ import { useIdentityActions } from "../identity/use-identity-actions.js";
|
|
|
7
7
|
* `ReactiveDataProvider`, structurally identical to `CartActionProvider`. The compiler wraps every
|
|
8
8
|
* element whose definition declares `actions: { identity: [...] }` with
|
|
9
9
|
* `<IdentityActionProvider methods={[...]}>`, calls `useIdentityActions` once, narrows the surface to
|
|
10
|
-
* the declared `methods`, and
|
|
11
|
-
*
|
|
10
|
+
* the declared `methods`, and publishes it through `ActionsOverrideContext` (merging with any outer
|
|
11
|
+
* namespace provider) for `useActionsOverride` to read further down.
|
|
12
12
|
*/
|
|
13
13
|
function IdentityActionProvider(props) {
|
|
14
|
-
return
|
|
14
|
+
return useInjectActions("identity", useIdentityActions(), props);
|
|
15
15
|
}
|
|
16
16
|
//#endregion
|
|
17
17
|
export { IdentityActionProvider };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-action-provider.js","names":[],"sources":["../../../src/sdk/actions/identity-action-provider.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport { useIdentityActions } from '../identity/use-identity-actions.js';\nimport { type ActionProviderProps,
|
|
1
|
+
{"version":3,"file":"identity-action-provider.js","names":[],"sources":["../../../src/sdk/actions/identity-action-provider.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport { useIdentityActions } from '../identity/use-identity-actions.js';\nimport { type ActionProviderProps, useInjectActions } from './inject-actions.js';\n\nexport type IdentityActionProviderProps = ActionProviderProps;\n\n/**\n * Compiler-emitted runtime wrapper for the `identity` action namespace — the write-side mirror of\n * `ReactiveDataProvider`, structurally identical to `CartActionProvider`. The compiler wraps every\n * element whose definition declares `actions: { identity: [...] }` with\n * `<IdentityActionProvider methods={[...]}>`, calls `useIdentityActions` once, narrows the surface to\n * the declared `methods`, and publishes it through `ActionsOverrideContext` (merging with any outer\n * namespace provider) for `useActionsOverride` to read further down.\n */\nexport function IdentityActionProvider(props: IdentityActionProviderProps): ReactNode {\n return useInjectActions('identity', useIdentityActions(), props);\n}\n"],"mappings":";;;;;;;;;;;;AAgBA,SAAgB,uBAAuB,OAA+C;CACpF,OAAO,iBAAiB,YAAY,mBAAmB,GAAG,KAAK;AACjE"}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { ReactNode } from "react";
|
|
2
2
|
//#region src/sdk/actions/inject-actions.d.ts
|
|
3
|
-
/** Props every compiler-emitted action provider takes
|
|
3
|
+
/** Props every compiler-emitted action provider takes. */
|
|
4
4
|
export type ActionProviderProps = Readonly<{
|
|
5
5
|
/** Method names the consuming component declared under `actions: { <namespace>: [...] }`. */
|
|
6
6
|
methods: readonly string[];
|
|
7
7
|
children: ReactNode;
|
|
8
|
-
/** Injected by the content boundary when the consumer also declares a content model. */
|
|
9
|
-
$content?: Record<string, unknown>;
|
|
10
|
-
/** Injected by another namespace's provider when the consumer declares more than one. */
|
|
11
|
-
$actions?: Record<string, Record<string, unknown>>;
|
|
12
8
|
}>;
|
|
13
9
|
//#endregion
|
|
14
10
|
//# sourceMappingURL=inject-actions.d.ts.map
|
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { ActionsOverrideContext, mergeComponentActions } from "./actions-override-context.js";
|
|
3
|
+
import { useContext } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
5
|
//#region src/sdk/actions/inject-actions.tsx
|
|
4
6
|
/**
|
|
5
|
-
* The shared body of every action provider: narrow the namespace surface to the declared `methods
|
|
6
|
-
*
|
|
7
|
+
* The shared body of every action provider: narrow the namespace surface to the declared `methods`,
|
|
8
|
+
* merge it onto whatever `ActionsOverrideContext` already carries, and publish the merged result to
|
|
9
|
+
* `children` through context — never by inspecting or cloning `children` itself.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* `children` used to be cloned onto directly (`isValidElement` + `cloneElement`), mirroring how
|
|
12
|
+
* `$actions` was destructured as a prop. That broke under React 19 RSC: `children` crosses the
|
|
13
|
+
* Server → Client flight boundary as a `Symbol(react.lazy)` reference on some renders, which
|
|
14
|
+
* `isValidElement` rejects, so the clone silently never ran and `$actions` stayed `undefined` for the
|
|
15
|
+
* whole page load. Publishing through context instead means `children`'s runtime shape never gates
|
|
16
|
+
* delivery — `children` always renders, resolved or not.
|
|
17
|
+
*
|
|
18
|
+
* Named `use...` (a hook, not a plain helper) because it calls `useContext` itself — every provider
|
|
19
|
+
* calls it directly from its own render, so the call sits at that hook's top level either way.
|
|
12
20
|
*/
|
|
13
|
-
function
|
|
14
|
-
|
|
21
|
+
function useInjectActions(namespace, surface, props) {
|
|
22
|
+
const { methods, children } = props;
|
|
23
|
+
const ambient = useContext(ActionsOverrideContext);
|
|
15
24
|
const selected = {};
|
|
16
25
|
for (const method of methods) {
|
|
17
26
|
const handler = surface[method];
|
|
18
27
|
if (typeof handler === "function") selected[method] = handler;
|
|
19
28
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
return cloneElement(child, {
|
|
26
|
-
...$content === void 0 ? {} : { $content },
|
|
27
|
-
$actions: {
|
|
28
|
-
...inherited,
|
|
29
|
-
[namespace]: selected
|
|
30
|
-
}
|
|
29
|
+
const merged = mergeComponentActions(ambient, { [namespace]: selected });
|
|
30
|
+
return /* @__PURE__ */ jsx(ActionsOverrideContext, {
|
|
31
|
+
value: merged,
|
|
32
|
+
children
|
|
31
33
|
});
|
|
32
34
|
}
|
|
33
35
|
//#endregion
|
|
34
|
-
export {
|
|
36
|
+
export { useInjectActions };
|
|
35
37
|
|
|
36
38
|
//# sourceMappingURL=inject-actions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inject-actions.js","names":[],"sources":["../../../src/sdk/actions/inject-actions.tsx"],"sourcesContent":["'use client';\n\nimport {
|
|
1
|
+
{"version":3,"file":"inject-actions.js","names":[],"sources":["../../../src/sdk/actions/inject-actions.tsx"],"sourcesContent":["'use client';\n\nimport { type ReactNode, useContext } from 'react';\nimport { ActionsOverrideContext, mergeComponentActions } from './actions-override-context.js';\nimport type { ComponentActions } from './types.js';\n\n/** Props every compiler-emitted action provider takes. */\nexport type ActionProviderProps = Readonly<{\n /** Method names the consuming component declared under `actions: { <namespace>: [...] }`. */\n methods: readonly string[];\n children: ReactNode;\n}>;\n\n/**\n * The shared body of every action provider: narrow the namespace surface to the declared `methods`,\n * merge it onto whatever `ActionsOverrideContext` already carries, and publish the merged result to\n * `children` through context — never by inspecting or cloning `children` itself.\n *\n * `children` used to be cloned onto directly (`isValidElement` + `cloneElement`), mirroring how\n * `$actions` was destructured as a prop. That broke under React 19 RSC: `children` crosses the\n * Server → Client flight boundary as a `Symbol(react.lazy)` reference on some renders, which\n * `isValidElement` rejects, so the clone silently never ran and `$actions` stayed `undefined` for the\n * whole page load. Publishing through context instead means `children`'s runtime shape never gates\n * delivery — `children` always renders, resolved or not.\n *\n * Named `use...` (a hook, not a plain helper) because it calls `useContext` itself — every provider\n * calls it directly from its own render, so the call sits at that hook's top level either way.\n */\nexport function useInjectActions(namespace: string, surface: Record<string, unknown>, props: ActionProviderProps) {\n const { methods, children } = props;\n const ambient = useContext(ActionsOverrideContext);\n\n const selected: Record<string, unknown> = {};\n for (const method of methods) {\n const handler = surface[method];\n if (typeof handler === 'function') selected[method] = handler;\n }\n\n const merged = mergeComponentActions(ambient, { [namespace]: selected } as ComponentActions);\n\n return <ActionsOverrideContext value={merged}>{children}</ActionsOverrideContext>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,iBAAiB,WAAmB,SAAkC,OAA4B;CAChH,MAAM,EAAE,SAAS,aAAa;CAC9B,MAAM,UAAU,WAAW,sBAAsB;CAEjD,MAAM,WAAoC,CAAC;CAC3C,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,UAAU,QAAQ;EACxB,IAAI,OAAO,YAAY,YAAY,SAAS,UAAU;CACxD;CAEA,MAAM,SAAS,sBAAsB,SAAS,GAAG,YAAY,SAAS,CAAqB;CAE3F,OAAO,oBAAC,wBAAD;EAAwB,OAAO;EAAS;CAAiC,CAAA;AAClF"}
|
|
@@ -6,9 +6,10 @@ export type RegionActionProviderProps = ActionProviderProps;
|
|
|
6
6
|
* Compiler-emitted runtime wrapper for the `region` action namespace — a line-for-line mirror of
|
|
7
7
|
* `SessionActionProvider`. The compiler wraps every element whose definition declares
|
|
8
8
|
* `actions: { region: [...] }` with `<RegionActionProvider methods={[...]}>`, calls `useRegionActions`
|
|
9
|
-
* once, narrows the surface to the declared `methods`, and
|
|
10
|
-
*
|
|
11
|
-
* declare is never wrapped, so its hook is never
|
|
9
|
+
* once, narrows the surface to the declared `methods`, and publishes it through
|
|
10
|
+
* `ActionsOverrideContext` (merging with any outer namespace provider) for `useActionsOverride` to
|
|
11
|
+
* read further down. A namespace a component does not declare is never wrapped, so its hook is never
|
|
12
|
+
* called.
|
|
12
13
|
*/
|
|
13
14
|
export declare function RegionActionProvider(props: RegionActionProviderProps): ReactNode;
|
|
14
15
|
//#endregion
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { useInjectActions } from "./inject-actions.js";
|
|
3
3
|
import { useRegionActions } from "../region/use-region-actions.js";
|
|
4
4
|
//#region src/sdk/actions/region-action-provider.tsx
|
|
5
5
|
/**
|
|
6
6
|
* Compiler-emitted runtime wrapper for the `region` action namespace — a line-for-line mirror of
|
|
7
7
|
* `SessionActionProvider`. The compiler wraps every element whose definition declares
|
|
8
8
|
* `actions: { region: [...] }` with `<RegionActionProvider methods={[...]}>`, calls `useRegionActions`
|
|
9
|
-
* once, narrows the surface to the declared `methods`, and
|
|
10
|
-
*
|
|
11
|
-
* declare is never wrapped, so its hook is never
|
|
9
|
+
* once, narrows the surface to the declared `methods`, and publishes it through
|
|
10
|
+
* `ActionsOverrideContext` (merging with any outer namespace provider) for `useActionsOverride` to
|
|
11
|
+
* read further down. A namespace a component does not declare is never wrapped, so its hook is never
|
|
12
|
+
* called.
|
|
12
13
|
*/
|
|
13
14
|
function RegionActionProvider(props) {
|
|
14
|
-
return
|
|
15
|
+
return useInjectActions("region", useRegionActions(), props);
|
|
15
16
|
}
|
|
16
17
|
//#endregion
|
|
17
18
|
export { RegionActionProvider };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"region-action-provider.js","names":[],"sources":["../../../src/sdk/actions/region-action-provider.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport { useRegionActions } from '../region/use-region-actions.js';\nimport { type ActionProviderProps,
|
|
1
|
+
{"version":3,"file":"region-action-provider.js","names":[],"sources":["../../../src/sdk/actions/region-action-provider.tsx"],"sourcesContent":["'use client';\n\nimport type { ReactNode } from 'react';\nimport { useRegionActions } from '../region/use-region-actions.js';\nimport { type ActionProviderProps, useInjectActions } from './inject-actions.js';\n\nexport type RegionActionProviderProps = ActionProviderProps;\n\n/**\n * Compiler-emitted runtime wrapper for the `region` action namespace — a line-for-line mirror of\n * `SessionActionProvider`. The compiler wraps every element whose definition declares\n * `actions: { region: [...] }` with `<RegionActionProvider methods={[...]}>`, calls `useRegionActions`\n * once, narrows the surface to the declared `methods`, and publishes it through\n * `ActionsOverrideContext` (merging with any outer namespace provider) for `useActionsOverride` to\n * read further down. A namespace a component does not declare is never wrapped, so its hook is never\n * called.\n */\nexport function RegionActionProvider(props: RegionActionProviderProps): ReactNode {\n return useInjectActions('region', useRegionActions(), props);\n}\n"],"mappings":";;;;;;;;;;;;;AAiBA,SAAgB,qBAAqB,OAA6C;CAChF,OAAO,iBAAiB,UAAU,iBAAiB,GAAG,KAAK;AAC7D"}
|
|
@@ -6,9 +6,10 @@ export type SessionActionProviderProps = ActionProviderProps;
|
|
|
6
6
|
* Compiler-emitted runtime wrapper for the `session` action namespace — a line-for-line mirror of
|
|
7
7
|
* `CartActionProvider`. The compiler wraps every element whose definition declares
|
|
8
8
|
* `actions: { session: [...] }` with `<SessionActionProvider methods={[...]}>`, calls
|
|
9
|
-
* `useSessionActions` once, narrows the surface to the declared `methods`, and
|
|
10
|
-
*
|
|
11
|
-
* component does not declare is never wrapped, so its hook is
|
|
9
|
+
* `useSessionActions` once, narrows the surface to the declared `methods`, and publishes it through
|
|
10
|
+
* `ActionsOverrideContext` (merging with any outer namespace provider) for `useActionsOverride` to
|
|
11
|
+
* read further down. A namespace a component does not declare is never wrapped, so its hook is
|
|
12
|
+
* never called (specs/actions.md FR-6).
|
|
12
13
|
*/
|
|
13
14
|
export declare function SessionActionProvider(props: SessionActionProviderProps): ReactNode;
|
|
14
15
|
//#endregion
|