@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.
Files changed (94) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +4 -2
  3. package/dist/catalog.json +286 -265
  4. package/dist/components.d.ts +2 -1
  5. package/dist/components.js +2 -1
  6. package/dist/content-types.json +16 -0
  7. package/dist/core/dist/actions/registry.d.ts +1 -1
  8. package/dist/core/dist/actions/registry.js +1 -1
  9. package/dist/core/dist/actions/registry.js.map +1 -1
  10. package/dist/docs/account-entry-point.md +1 -1
  11. package/dist/docs/cart.md +2 -2
  12. package/dist/docs/session.md +12 -6
  13. package/dist/docs/sign-in-redirect.md +137 -0
  14. package/dist/matter/access-code-sign-in/access-code-sign-in.d.ts +2 -2
  15. package/dist/matter/access-code-sign-in/access-code-sign-in.js +5 -2
  16. package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
  17. package/dist/matter/account-entry-point/account-entry-point.js +4 -16
  18. package/dist/matter/account-entry-point/account-entry-point.js.map +1 -1
  19. package/dist/matter/buy-button/buy-button.d.ts +1 -1
  20. package/dist/matter/buy-button/buy-button.js +3 -1
  21. package/dist/matter/buy-button/buy-button.js.map +1 -1
  22. package/dist/matter/cart/cart.d.ts +6 -6
  23. package/dist/matter/cart/cart.js +13 -6
  24. package/dist/matter/cart/cart.js.map +1 -1
  25. package/dist/matter/cart-item/cart-item.d.ts +2 -2
  26. package/dist/matter/cart-item/cart-item.js +5 -2
  27. package/dist/matter/cart-item/cart-item.js.map +1 -1
  28. package/dist/matter/client-preferences/client-preferences.js +3 -1
  29. package/dist/matter/client-preferences/client-preferences.js.map +1 -1
  30. package/dist/matter/client-profile/client-profile.d.ts +1 -1
  31. package/dist/matter/client-profile/client-profile.js +3 -1
  32. package/dist/matter/client-profile/client-profile.js.map +1 -1
  33. package/dist/matter/client-profile-form/client-profile-form.d.ts +1 -1
  34. package/dist/matter/client-profile-form/client-profile-form.js +3 -1
  35. package/dist/matter/client-profile-form/client-profile-form.js.map +1 -1
  36. package/dist/matter/delivery-options/delivery-options-accordion.d.ts +1 -1
  37. package/dist/matter/delivery-options/delivery-options-accordion.js +3 -1
  38. package/dist/matter/delivery-options/delivery-options-accordion.js.map +1 -1
  39. package/dist/matter/delivery-options/delivery-options-controller.d.ts +1 -1
  40. package/dist/matter/delivery-options/delivery-options-controller.js +3 -1
  41. package/dist/matter/delivery-options/delivery-options-controller.js.map +1 -1
  42. package/dist/matter/delivery-options/delivery-options.d.ts +1 -1
  43. package/dist/matter/delivery-options/delivery-options.js +3 -1
  44. package/dist/matter/delivery-options/delivery-options.js.map +1 -1
  45. package/dist/matter/email-identification/email-identification.d.ts +1 -1
  46. package/dist/matter/email-identification/email-identification.js +3 -1
  47. package/dist/matter/email-identification/email-identification.js.map +1 -1
  48. package/dist/matter/identity-summary/identity-summary.d.ts +1 -1
  49. package/dist/matter/identity-summary/identity-summary.js +3 -1
  50. package/dist/matter/identity-summary/identity-summary.js.map +1 -1
  51. package/dist/matter/product-list/product-list.d.ts +1 -1
  52. package/dist/matter/product-list/product-list.js +3 -1
  53. package/dist/matter/product-list/product-list.js.map +1 -1
  54. package/dist/matter/region/region.d.ts +1 -1
  55. package/dist/matter/region/region.js +5 -2
  56. package/dist/matter/region/region.js.map +1 -1
  57. package/dist/matter/session/session.js +1 -1
  58. package/dist/matter/session/session.js.map +1 -1
  59. package/dist/matter/shipping-address/shipping-address.d.ts +1 -1
  60. package/dist/matter/shipping-address/shipping-address.js +3 -1
  61. package/dist/matter/shipping-address/shipping-address.js.map +1 -1
  62. package/dist/matter/sign-in-form/sign-in-form.d.ts +1 -1
  63. package/dist/matter/sign-in-form/sign-in-form.js +3 -1
  64. package/dist/matter/sign-in-form/sign-in-form.js.map +1 -1
  65. package/dist/matter/sign-in-redirect/sign-in-redirect.d.ts +45 -0
  66. package/dist/matter/sign-in-redirect/sign-in-redirect.js +127 -0
  67. package/dist/matter/sign-in-redirect/sign-in-redirect.js.map +1 -0
  68. package/dist/sdk/actions/actions-override-context.js +37 -0
  69. package/dist/sdk/actions/actions-override-context.js.map +1 -0
  70. package/dist/sdk/actions/cart-action-provider.d.ts +8 -6
  71. package/dist/sdk/actions/cart-action-provider.js +10 -8
  72. package/dist/sdk/actions/cart-action-provider.js.map +1 -1
  73. package/dist/sdk/actions/identity-action-provider.d.ts +2 -2
  74. package/dist/sdk/actions/identity-action-provider.js +4 -4
  75. package/dist/sdk/actions/identity-action-provider.js.map +1 -1
  76. package/dist/sdk/actions/inject-actions.d.ts +1 -5
  77. package/dist/sdk/actions/inject-actions.js +23 -21
  78. package/dist/sdk/actions/inject-actions.js.map +1 -1
  79. package/dist/sdk/actions/region-action-provider.d.ts +4 -3
  80. package/dist/sdk/actions/region-action-provider.js +6 -5
  81. package/dist/sdk/actions/region-action-provider.js.map +1 -1
  82. package/dist/sdk/actions/session-action-provider.d.ts +4 -3
  83. package/dist/sdk/actions/session-action-provider.js +6 -5
  84. package/dist/sdk/actions/session-action-provider.js.map +1 -1
  85. package/dist/sdk/actions/shipping-action-provider.d.ts +4 -3
  86. package/dist/sdk/actions/shipping-action-provider.js +6 -5
  87. package/dist/sdk/actions/shipping-action-provider.js.map +1 -1
  88. package/dist/sdk/cart/use-cart-actions.js.map +1 -1
  89. package/dist/sdk/client/create-client.js +24 -2
  90. package/dist/sdk/client/create-client.js.map +1 -1
  91. package/dist/sdk/content/create-content-runtime.js +1 -1
  92. package/dist/utils/safe-href.js +107 -1
  93. package/dist/utils/safe-href.js.map +1 -1
  94. 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 threads it into the single child via
10
- * `$actions.cart` (merging with any outer namespace provider). A namespace a component does not
11
- * declare is never wrapped, so its hook is never called (specs/actions.md FR-6).
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 injects `$actions` as a prop (cloneElement) rather than a context so the contract matches
14
- * `$data`: components destructure `$actions` from props and stay directly testable with `mockAction`.
15
- * That makes the child's shape load-bearing — see `injectActions`.
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 { injectActions } from "./inject-actions.js";
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 threads it into the single child via
10
- * `$actions.cart` (merging with any outer namespace provider). A namespace a component does not
11
- * declare is never wrapped, so its hook is never called (specs/actions.md FR-6).
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 injects `$actions` as a prop (cloneElement) rather than a context so the contract matches
14
- * `$data`: components destructure `$actions` from props and stay directly testable with `mockAction`.
15
- * That makes the child's shape load-bearing — see `injectActions`.
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 injectActions("cart", useCartActions(), props);
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, injectActions } 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 threads it into the single child via\n * `$actions.cart` (merging with any outer namespace provider). A namespace a component does not\n * declare is never wrapped, so its hook is never called (specs/actions.md FR-6).\n *\n * It injects `$actions` as a prop (cloneElement) rather than a context so the contract matches\n * `$data`: components destructure `$actions` from props and stay directly testable with `mockAction`.\n * That makes the child's shape load-bearing — see `injectActions`.\n */\nexport function CartActionProvider(props: CartActionProviderProps): ReactNode {\n return injectActions('cart', useCartActions(), props);\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAoBA,SAAgB,mBAAmB,OAA2C;CAC5E,OAAO,cAAc,QAAQ,eAAe,GAAG,KAAK;AACtD"}
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 threads it into the single child via `$actions.identity` (merging with
11
- * any outer namespace provider).
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 { injectActions } from "./inject-actions.js";
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 threads it into the single child via `$actions.identity` (merging with
11
- * any outer namespace provider).
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 injectActions("identity", useIdentityActions(), props);
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, injectActions } 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 threads it into the single child via `$actions.identity` (merging with\n * any outer namespace provider).\n */\nexport function IdentityActionProvider(props: IdentityActionProviderProps): ReactNode {\n return injectActions('identity', useIdentityActions(), props);\n}\n"],"mappings":";;;;;;;;;;;;AAgBA,SAAgB,uBAAuB,OAA+C;CACpF,OAAO,cAAc,YAAY,mBAAmB,GAAG,KAAK;AAC9D"}
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, plus whatever an outer wrapper injected. */
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 { cloneElement, isValidElement } from "react";
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
- * and clone it onto the single child as `$actions.<namespace>`.
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
- * A provider is transparent, so it forwards what an outer wrapper injected onto *it* rather than
9
- * consuming it: `$content` from the content boundary, and the namespaces of an outer provider when
10
- * the consumer declares more than one. Reading `$actions` off the child as well keeps a child that
11
- * already carries some (a hand-authored tree, `mockAction` in a test) working.
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 injectActions(namespace, surface, { methods, children, $content, $actions }) {
14
- if (!isValidElement(children)) return children;
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 child = children;
21
- const inherited = {
22
- ...child.props.$actions,
23
- ...$actions
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 { injectActions };
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 { cloneElement, isValidElement, type ReactElement, type ReactNode } from 'react';\n\n/** Props every compiler-emitted action provider takes, plus whatever an outer wrapper injected. */\nexport type ActionProviderProps = Readonly<{\n /** Method names the consuming component declared under `actions: { <namespace>: [...] }`. */\n methods: readonly string[];\n children: ReactNode;\n /** Injected by the content boundary when the consumer also declares a content model. */\n $content?: Record<string, unknown>;\n /** Injected by another namespace's provider when the consumer declares more than one. */\n $actions?: Record<string, Record<string, unknown>>;\n}>;\n\ntype InjectedProps = Pick<ActionProviderProps, '$content' | '$actions'>;\n\n/**\n * The shared body of every action provider: narrow the namespace surface to the declared `methods`\n * and clone it onto the single child as `$actions.<namespace>`.\n *\n * A provider is transparent, so it forwards what an outer wrapper injected onto *it* rather than\n * consuming it: `$content` from the content boundary, and the namespaces of an outer provider when\n * the consumer declares more than one. Reading `$actions` off the child as well keeps a child that\n * already carries some (a hand-authored tree, `mockAction` in a test) working.\n */\nexport function injectActions(\n namespace: string,\n surface: Record<string, unknown>,\n { methods, children, $content, $actions }: ActionProviderProps,\n): ReactNode {\n if (!isValidElement(children)) return children;\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 child = children as ReactElement<InjectedProps>;\n const inherited = { ...child.props.$actions, ...$actions };\n return cloneElement(child, {\n ...($content === undefined ? {} : { $content }),\n $actions: { ...inherited, [namespace]: selected },\n });\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,SAAgB,cACd,WACA,SACA,EAAE,SAAS,UAAU,UAAU,YACpB;CACX,IAAI,CAAC,eAAe,QAAQ,GAAG,OAAO;CAEtC,MAAM,WAAoC,CAAC;CAC3C,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,UAAU,QAAQ;EACxB,IAAI,OAAO,YAAY,YAAY,SAAS,UAAU;CACxD;CAEA,MAAM,QAAQ;CACd,MAAM,YAAY;EAAE,GAAG,MAAM,MAAM;EAAU,GAAG;CAAS;CACzD,OAAO,aAAa,OAAO;EACzB,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS;EAC7C,UAAU;GAAE,GAAG;IAAY,YAAY;EAAS;CAClD,CAAC;AACH"}
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 threads it into the single child via
10
- * `$actions.region` (merging with any outer namespace provider). A namespace a component does not
11
- * declare is never wrapped, so its hook is never called.
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 { injectActions } from "./inject-actions.js";
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 threads it into the single child via
10
- * `$actions.region` (merging with any outer namespace provider). A namespace a component does not
11
- * declare is never wrapped, so its hook is never called.
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 injectActions("region", useRegionActions(), props);
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, injectActions } 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 threads it into the single child via\n * `$actions.region` (merging with any outer namespace provider). A namespace a component does not\n * declare is never wrapped, so its hook is never called.\n */\nexport function RegionActionProvider(props: RegionActionProviderProps): ReactNode {\n return injectActions('region', useRegionActions(), props);\n}\n"],"mappings":";;;;;;;;;;;;AAgBA,SAAgB,qBAAqB,OAA6C;CAChF,OAAO,cAAc,UAAU,iBAAiB,GAAG,KAAK;AAC1D"}
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 threads it into the
10
- * single child via `$actions.session` (merging with any outer namespace provider). A namespace a
11
- * component does not declare is never wrapped, so its hook is never called (specs/actions.md FR-6).
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