@vtex/faststore 0.0.5 → 0.0.6
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 +45 -0
- package/dist/actions.json +181 -0
- package/dist/catalog.json +1543 -495
- package/dist/client.d.ts +2 -1
- package/dist/client.js +2 -1
- package/dist/components.d.ts +28 -7
- package/dist/components.js +31 -12
- package/dist/docs/access-code-sign-in.md +1 -1
- package/dist/docs/address-summary.md +24 -3
- package/dist/docs/alert-dialog.md +204 -0
- package/dist/docs/billing-address.md +284 -0
- package/dist/docs/card-entry.md +250 -0
- package/dist/docs/cardholder-document.md +142 -0
- package/dist/docs/client-preferences-fieldset.md +148 -0
- package/dist/docs/client-preferences.md +2 -1
- package/dist/docs/client-profile-fieldset.md +184 -0
- package/dist/docs/client-profile.md +1 -1
- package/dist/docs/document-field.md +58 -4
- package/dist/docs/field.md +317 -0
- package/dist/docs/form.md +307 -0
- package/dist/docs/identity-edit.md +6 -6
- package/dist/docs/identity-summary.md +5 -5
- package/dist/docs/installments.md +248 -0
- package/dist/docs/my-account-profile.md +1 -0
- package/dist/docs/order-placed-payment-summary.md +138 -0
- package/dist/docs/payment-method-badges.md +232 -0
- package/dist/docs/payment-methods.md +1 -0
- package/dist/docs/payment.md +218 -0
- package/dist/docs/phone-field.md +58 -4
- package/dist/docs/place-order.md +157 -0
- package/dist/docs/save-card.md +186 -0
- package/dist/docs/saved-cards.md +218 -0
- package/dist/docs/select.md +465 -0
- package/dist/docs/shipping-address.md +110 -258
- package/dist/index.d.ts +4 -1
- package/dist/internal/client.d.ts +3 -1
- package/dist/internal/client.js +3 -1
- package/dist/matter/access-code-sign-in/access-code-sign-in.js +9 -8
- package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
- package/dist/matter/billing-address/billing-address.d.ts +165 -0
- package/dist/matter/billing-address/billing-address.js +408 -0
- package/dist/matter/billing-address/billing-address.js.map +1 -0
- package/dist/matter/card-entry/card-capture.d.ts +25 -0
- package/dist/matter/card-entry/card-capture.js +25 -0
- package/dist/matter/card-entry/card-capture.js.map +1 -0
- package/dist/matter/card-entry/card-entry.d.ts +120 -0
- package/dist/matter/card-entry/card-entry.js +832 -0
- package/dist/matter/card-entry/card-entry.js.map +1 -0
- package/dist/matter/cardholder-document/cardholder-document.d.ts +45 -0
- package/dist/matter/cardholder-document/cardholder-document.js +70 -0
- package/dist/matter/cardholder-document/cardholder-document.js.map +1 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.d.ts +77 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.js +299 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.js.map +1 -0
- package/dist/matter/client-preferences/client-preferences.d.ts +3 -5
- package/dist/matter/client-preferences/client-preferences.js +6 -6
- package/dist/matter/client-preferences/client-preferences.js.map +1 -1
- package/dist/matter/client-profile/client-profile.d.ts +4 -6
- package/dist/matter/client-profile/client-profile.js +9 -6
- package/dist/matter/client-profile/client-profile.js.map +1 -1
- package/dist/matter/client-profile-form/client-profile-fieldset.d.ts +119 -0
- package/dist/matter/client-profile-form/client-profile-fieldset.js +488 -0
- package/dist/matter/client-profile-form/client-profile-fieldset.js.map +1 -0
- package/dist/matter/identity-edit/identity-edit.js +2 -2
- package/dist/matter/identity-edit/identity-edit.js.map +1 -1
- package/dist/matter/identity-summary/identity-summary.js +2 -2
- package/dist/matter/identity-summary/identity-summary.js.map +1 -1
- package/dist/matter/installments/installments-select.d.ts +42 -0
- package/dist/matter/installments/installments-select.js +167 -0
- package/dist/matter/installments/installments-select.js.map +1 -0
- package/dist/matter/installments/installments.d.ts +58 -0
- package/dist/matter/installments/installments.js +176 -0
- package/dist/matter/installments/installments.js.map +1 -0
- package/dist/matter/installments/use-installments-selection.d.ts +29 -0
- package/dist/matter/installments/use-installments-selection.js +95 -0
- package/dist/matter/installments/use-installments-selection.js.map +1 -0
- package/dist/matter/my-account-profile/my-account-profile.js +4 -4
- package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.d.ts +63 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js +305 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js.map +1 -0
- package/dist/matter/payment/payment-card-capture.d.ts +19 -0
- package/dist/matter/payment/payment-card-capture.js +129 -0
- package/dist/matter/payment/payment-card-capture.js.map +1 -0
- package/dist/matter/payment/payment-controller.d.ts +24 -0
- package/dist/matter/payment/payment-controller.js +107 -0
- package/dist/matter/payment/payment-controller.js.map +1 -0
- package/dist/matter/payment/payment-method-badges.d.ts +57 -0
- package/dist/matter/payment/payment-method-badges.js +192 -0
- package/dist/matter/payment/payment-method-badges.js.map +1 -0
- package/dist/matter/payment/payment.d.ts +89 -0
- package/dist/matter/payment/payment.js +263 -0
- package/dist/matter/payment/payment.js.map +1 -0
- package/dist/matter/payment/use-payment-method-selection.d.ts +302 -0
- package/dist/matter/payment/use-payment-method-selection.js +458 -0
- package/dist/matter/payment/use-payment-method-selection.js.map +1 -0
- package/dist/matter/payment/use-payment-submit-gate.d.ts +111 -0
- package/dist/matter/payment/use-payment-submit-gate.js +144 -0
- package/dist/matter/payment/use-payment-submit-gate.js.map +1 -0
- package/dist/matter/place-order/place-order.d.ts +74 -0
- package/dist/matter/place-order/place-order.js +379 -0
- package/dist/matter/place-order/place-order.js.map +1 -0
- package/dist/matter/product-list/product-list.d.ts +1 -0
- package/dist/matter/product-list/product-list.js.map +1 -1
- package/dist/matter/product-shelf/product-shelf.d.ts +1 -0
- package/dist/matter/product-shelf/product-shelf.js +2 -0
- package/dist/matter/product-shelf/product-shelf.js.map +1 -1
- package/dist/matter/region/region.js +1 -1
- package/dist/matter/region/region.js.map +1 -1
- package/dist/matter/save-card/save-card.d.ts +49 -0
- package/dist/matter/save-card/save-card.js +188 -0
- package/dist/matter/save-card/save-card.js.map +1 -0
- package/dist/matter/saved-cards/saved-cards.d.ts +79 -0
- package/dist/matter/saved-cards/saved-cards.js +387 -0
- package/dist/matter/saved-cards/saved-cards.js.map +1 -0
- package/dist/matter/shipping-address/country-descriptors.js +20 -10
- package/dist/matter/shipping-address/country-descriptors.js.map +1 -1
- package/dist/matter/shipping-address/shipping-address.d.ts +113 -174
- package/dist/matter/shipping-address/shipping-address.js +480 -791
- package/dist/matter/shipping-address/shipping-address.js.map +1 -1
- package/dist/matter/variant-selector/variant-selector.js +1 -1
- package/dist/particles/address-summary/address-summary.d.ts +24 -0
- package/dist/particles/address-summary/address-summary.js +83 -2
- package/dist/particles/address-summary/address-summary.js.map +1 -1
- package/dist/particles/alert-dialog/alert-dialog.d.ts +68 -0
- package/dist/particles/alert-dialog/alert-dialog.js +187 -0
- package/dist/particles/alert-dialog/alert-dialog.js.map +1 -0
- package/dist/particles/checkbox/checkbox.d.ts +7 -3
- package/dist/particles/checkbox/checkbox.js +4 -1
- package/dist/particles/checkbox/checkbox.js.map +1 -1
- package/dist/particles/document-field/document-field.d.ts +48 -5
- package/dist/particles/document-field/document-field.js +189 -32
- package/dist/particles/document-field/document-field.js.map +1 -1
- package/dist/particles/field/bound-field-registry.js +52 -0
- package/dist/particles/field/bound-field-registry.js.map +1 -0
- package/dist/particles/field/field.d.ts +116 -0
- package/dist/particles/field/field.js +497 -6
- package/dist/particles/field/field.js.map +1 -1
- package/dist/particles/fieldset/fieldset.js +2 -2
- package/dist/particles/fieldset/fieldset.js.map +1 -1
- package/dist/particles/form/form.d.ts +104 -0
- package/dist/particles/form/form.js +485 -0
- package/dist/particles/form/form.js.map +1 -0
- package/dist/particles/phone-field/phone-field.d.ts +38 -10
- package/dist/particles/phone-field/phone-field.js +183 -39
- package/dist/particles/phone-field/phone-field.js.map +1 -1
- package/dist/particles/postal-code-field/postal-code-field.js +2 -2
- package/dist/particles/postal-code-field/postal-code-field.js.map +1 -1
- package/dist/particles/select/field-select.d.ts +43 -0
- package/dist/particles/select/field-select.js +150 -0
- package/dist/particles/select/field-select.js.map +1 -0
- package/dist/particles/select/select.d.ts +2 -0
- package/dist/particles/select/select.js.map +1 -1
- package/dist/particles/shared/compose-refs.js +21 -0
- package/dist/particles/shared/compose-refs.js.map +1 -0
- package/dist/schema.graphql +701 -2
- package/dist/sdk/actions/analytics-action-provider.d.ts +15 -0
- package/dist/sdk/actions/analytics-action-provider.js +19 -0
- package/dist/sdk/actions/analytics-action-provider.js.map +1 -0
- package/dist/sdk/actions/index.d.ts +3 -1
- package/dist/sdk/actions/index.js +3 -1
- package/dist/sdk/actions/payment-action-provider.d.ts +22 -0
- package/dist/sdk/actions/payment-action-provider.js +26 -0
- package/dist/sdk/actions/payment-action-provider.js.map +1 -0
- package/dist/sdk/actions/types.d.ts +6 -2
- package/dist/sdk/analytics/analytics-destination.d.ts +43 -0
- package/dist/sdk/analytics/analytics-destination.js +90 -0
- package/dist/sdk/analytics/analytics-destination.js.map +1 -0
- package/dist/sdk/analytics/use-analytics-actions.d.ts +57 -0
- package/dist/sdk/analytics/use-analytics-actions.js +108 -0
- package/dist/sdk/analytics/use-analytics-actions.js.map +1 -0
- package/dist/sdk/cart/use-cart-actions.js +1 -1
- package/dist/sdk/data/entity-resolution-context.js +31 -0
- package/dist/sdk/data/entity-resolution-context.js.map +1 -0
- package/dist/sdk/data/entity-subscriber.js +18 -8
- package/dist/sdk/data/entity-subscriber.js.map +1 -1
- package/dist/sdk/data/index.js +1 -0
- package/dist/sdk/data/page-reactive-data-root.d.ts +36 -0
- package/dist/sdk/data/page-reactive-data-root.js +4 -2
- package/dist/sdk/data/page-reactive-data-root.js.map +1 -1
- package/dist/sdk/data/reactive-data-provider.js +7 -3
- package/dist/sdk/data/reactive-data-provider.js.map +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.js +12 -4
- package/dist/sdk/data/reactive-list-item-data-provider.js.map +1 -1
- package/dist/sdk/data/use-reactive-data-settled.js +39 -0
- package/dist/sdk/data/use-reactive-data-settled.js.map +1 -0
- package/dist/sdk/data/use-reactive-entity.js +5 -1
- package/dist/sdk/data/use-reactive-entity.js.map +1 -1
- package/dist/sdk/generated/fragment-types.js +2 -0
- package/dist/sdk/generated/fragment-types.js.map +1 -1
- package/dist/sdk/generated/graphql.js.map +1 -1
- package/dist/sdk/i18n/messages/en-US.js +3 -0
- package/dist/sdk/i18n/messages/en-US.json +3 -0
- package/dist/sdk/i18n/messages/en-US.json.d.ts +3 -0
- package/dist/sdk/i18n/messages/es-ES.js +3 -0
- package/dist/sdk/i18n/messages/es-ES.json +3 -0
- package/dist/sdk/i18n/messages/pt-BR.js +3 -0
- package/dist/sdk/i18n/messages/pt-BR.json +3 -0
- package/dist/sdk/identity/use-identity-actions.js +3 -2
- package/dist/sdk/identity/use-identity-actions.js.map +1 -1
- package/dist/sdk/payment/gateway-origin.js +35 -0
- package/dist/sdk/payment/gateway-origin.js.map +1 -0
- package/dist/sdk/payment/gateway.d.ts +85 -0
- package/dist/sdk/payment/gateway.js +183 -0
- package/dist/sdk/payment/gateway.js.map +1 -0
- package/dist/sdk/payment/operations.d.ts +52 -0
- package/dist/sdk/payment/operations.js +223 -0
- package/dist/sdk/payment/operations.js.map +1 -0
- package/dist/sdk/payment/security-code-mask.js +71 -0
- package/dist/sdk/payment/security-code-mask.js.map +1 -0
- package/dist/sdk/payment/submission-registry.d.ts +33 -0
- package/dist/sdk/payment/submission-registry.js +58 -0
- package/dist/sdk/payment/submission-registry.js.map +1 -0
- package/dist/sdk/payment/use-payment-actions.d.ts +78 -0
- package/dist/sdk/payment/use-payment-actions.js +276 -0
- package/dist/sdk/payment/use-payment-actions.js.map +1 -0
- package/dist/sdk/payment/use-place-order.d.ts +65 -0
- package/dist/sdk/payment/use-place-order.js +640 -0
- package/dist/sdk/payment/use-place-order.js.map +1 -0
- package/dist/sdk/shipping/use-shipping-actions.js +1 -1
- package/dist/utils/loggable-error-name.js +49 -0
- package/dist/utils/loggable-error-name.js.map +1 -0
- package/dist/utils/resolve-authored-copy.js +18 -0
- package/dist/utils/resolve-authored-copy.js.map +1 -0
- package/dist/utils/substitute-template.js +33 -0
- package/dist/utils/substitute-template.js.map +1 -0
- package/package.json +2 -1
- package/src/styles/tailwind.css +34 -0
- package/dist/docs/client-profile-form.md +0 -192
- package/dist/matter/client-profile-form/client-profile-form.d.ts +0 -126
- package/dist/matter/client-profile-form/client-profile-form.js +0 -662
- package/dist/matter/client-profile-form/client-profile-form.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-place-order.js","names":[],"sources":["../../../src/sdk/payment/use-place-order.ts"],"sourcesContent":["'use client';\n\nimport { useCallback, useMemo, useRef, useState } from 'react';\nimport { loggableErrorName } from '../../utils/loggable-error-name.js';\nimport type { ComponentActions } from '../actions/types.js';\nimport type { PaymentGatewayTransaction } from './gateway.js';\nimport { isTrustedGatewayOrigin } from './gateway-origin.js';\nimport { PAYMENT_NETWORK_ERROR_CODE, type PaymentMoney, type PaymentUserError } from './operations.js';\nimport { type PaymentPreflightCheck, type PaymentSubmission, usePaymentSubmission } from './submission-registry.js';\nimport type {\n PaymentActions,\n PaymentGatewayCallbackResult,\n PaymentStartTransactionResult,\n} from './use-payment-actions.js';\n\n/**\n * The place-order orchestration (specs/use-place-order.md, T13; docs/features/credit-card/rfc.md\n * § D9) — the PCI-critical core of the credit-card feature, and the only module that knows placing an\n * order is three calls rather than one mutation.\n *\n * **This hook never holds card data.** It resolves an opaque continuation from the submission channel\n * by the shopper's entry path and invokes it between the two backend legs; the card stays in the\n * holder's closure (T7 for a new card, T12 for a saved one), which calls\n * `$actions.payment.sendPayments` itself. So the actions this hook needs are `startTransaction`,\n * `gatewayCallback`, and `analytics.track` — never the gateway façade. Two tests assert the boundary\n * against this file's own source text: no card field is named in code, and the single import from\n * `./gateway.js` is the transaction descriptor's type.\n *\n * The order of the pre-flight is load-bearing, not stylistic. The path, the continuation, and the\n * selected method are all resolved *before* `startTransaction` runs, because a transaction that is\n * opened and then cannot be completed is dangling state on the shopper's order form.\n *\n * v1 is the direct-API mode: the gateway leg resolves inline and the shopper stays on the page. The\n * redirect mode — an external navigation with a resume-on-callback route, and with it 3D Secure — is\n * phase 2, which is why no `callbackUrl` reaches the descriptor.\n *\n * Every failure resolves as `userErrors`; nothing here throws for an expected outcome, so the CTA\n * (T14) renders one error path. Its one per-code branch is PLACE_ORDER_UNCONFIRMED, a pending outcome\n * it shows as a status rather than an error (T176).\n */\n\n// ---------------------------------------------------------------------------\n// Client-synthesized error codes\n// ---------------------------------------------------------------------------\n\n/** No entry path chosen, or no holder registered for the one that was. */\nexport const PAYMENT_MISSING_SUBMISSION_CODE = 'MISSING_PAYMENT_SUBMISSION';\n\n/**\n * No payment method selected. Reuses `PaymentStartTransactionErrorCode`'s own value for the same\n * state, so a consumer matches one code whichever side detected it.\n */\nexport const PAYMENT_MISSING_METHOD_CODE = 'MISSING_PAYMENT_METHOD_SELECTION';\n\n/**\n * The transaction opened but cannot be paid — an incomplete payload, or an origin this client will\n * not post a card to. Reuses the server's code for the same meaning (T18 Decision 5).\n */\nexport const PAYMENT_INVALID_TRANSACTION_CODE = 'INVALID_TRANSACTION_DATA';\n\n/**\n * The attempt's own sequence is still running — opened or finalizing, not yet settled. Not a\n * refusal: nothing failed, the record just belongs to a sequence this caller did not start.\n */\nexport const PLACE_ORDER_IN_FLIGHT_CODE = 'PLACE_ORDER_IN_FLIGHT';\n\n/**\n * The gatewayCallback leg ran after the gateway accepted the card POST, and this client could not confirm\n * its outcome — the payment may or may not have been authorized. Not a refusal (T176).\n */\nexport const PLACE_ORDER_UNCONFIRMED_CODE = 'PLACE_ORDER_UNCONFIRMED';\n\nconst MISSING_SUBMISSION_ERROR: PaymentUserError = {\n code: PAYMENT_MISSING_SUBMISSION_CODE,\n message: 'Enter your payment details before placing the order.',\n};\n\nconst MISSING_METHOD_ERROR: PaymentUserError = {\n code: PAYMENT_MISSING_METHOD_CODE,\n message: 'Choose a payment method before placing the order.',\n};\n\nconst INVALID_TRANSACTION_ERROR: PaymentUserError = {\n code: PAYMENT_INVALID_TRANSACTION_CODE,\n message: 'Unable to start the payment. Please try again.',\n};\n\nconst PLACE_ORDER_IN_FLIGHT_ERROR: PaymentUserError = {\n code: PLACE_ORDER_IN_FLIGHT_CODE,\n message: 'This order is already being placed. Please wait.',\n};\n\n/**\n * Worded as a pending state, never a failure: it must not tell the shopper to try again (FR-1).\n * `PlaceOrder`'s `unconfirmedMessage` schema default repeats this text verbatim.\n */\nconst PLACE_ORDER_UNCONFIRMED_ERROR: PaymentUserError = {\n code: PLACE_ORDER_UNCONFIRMED_CODE,\n message: 'We could not confirm your order yet. Your payment may still be processing, so please do not pay again.',\n};\n\n/** Mirrors `usePaymentActions`' own infrastructure-failure userError, which that module keeps private. */\nconst NETWORK_ERROR: PaymentUserError = {\n code: PAYMENT_NETWORK_ERROR_CODE,\n message: 'Unable to complete the request. Please try again.',\n};\n\n// ---------------------------------------------------------------------------\n// Data the descriptor needs from the read model\n// ---------------------------------------------------------------------------\n\nexport type PlaceOrderInstallment = Readonly<{\n count: number;\n /** Per-installment amount, as a display decimal. The gateway's unit is minor units. */\n value: PaymentMoney;\n interestRate: number;\n}>;\n\nexport type PlaceOrderMethod = Readonly<{ id: string; groupName: string }>;\n\n/**\n * The `Payment` slice T14's fragment forwards (arity-1, ADR-0015) — the same convention as\n * `PaymentSectionData`. These five values are the part of the descriptor `paymentStartTransaction`\n * does not carry: T18 deliberately did not re-project the read model onto a write payload.\n */\nexport type PlaceOrderData = {\n selectedMethodId?: string | null;\n selectedInstallment?: PlaceOrderInstallment | null;\n methods?: readonly PlaceOrderMethod[];\n} | null;\n\nexport type PlaceOrderResult = Readonly<{\n /** The placed order's id, once the callback resolved one. */\n orderId: string | null;\n /** Empty on success. Carries the server's own codes verbatim when a backend leg reported one. */\n userErrors: readonly PaymentUserError[];\n}>;\n\nexport type UsePlaceOrder = Readonly<{\n /** Runs the three-call sequence. Resolves; never rejects for an expected outcome. */\n placeOrder: () => Promise<PlaceOrderResult>;\n /** True while the sequence runs — what T14's blocking loading state renders. */\n placing: boolean;\n /** The latest attempt's userErrors. Empty before the first attempt and while one is in flight. */\n userErrors: readonly PaymentUserError[];\n /** The latest attempt's order id. */\n orderId: string | null;\n}>;\n\n// ---------------------------------------------------------------------------\n// Payload settling\n// ---------------------------------------------------------------------------\n\n/**\n * The start payload once every field the descriptor needs is known non-null. `userProfileId` is the\n * exception: a guest order legitimately has none.\n */\ntype SettledTransaction = Readonly<{\n transactionId: string;\n merchantName: string;\n gatewayUrl: string;\n orderGroup: string;\n userProfileId: string | null;\n value: PaymentMoney;\n valueInCents: number;\n referenceValueInCents: number;\n}>;\n\n/** Identifiers, a host, and amounts — nothing here is sensitive, so naming them in a log is safe. */\nfunction missingTransactionFields(payload: PaymentStartTransactionResult): string[] {\n return Object.entries({\n transactionId: payload.transactionId,\n merchantName: payload.merchantName,\n orderGroup: payload.orderGroup,\n value: payload.value,\n valueInCents: payload.valueInCents,\n referenceValueInCents: payload.referenceValueInCents,\n })\n .filter(([, field]) => field === null)\n .map(([name]) => name);\n}\n\n/**\n * Narrows a payload that reported no userErrors, or refuses it. The origin is checked first and\n * separately: it is the field that decides where a card is sent, and a refusal here happens before\n * any holder is asked to assemble one.\n */\nfunction settleTransaction(payload: PaymentStartTransactionResult): SettledTransaction | null {\n if (!isTrustedGatewayOrigin(payload.gatewayUrl)) {\n console.error('[payment.placeOrder] the opened transaction carries no trusted https gateway origin');\n return null;\n }\n\n const { transactionId, merchantName, orderGroup, userProfileId, value, valueInCents, referenceValueInCents } =\n payload;\n\n if (\n transactionId === null ||\n merchantName === null ||\n orderGroup === null ||\n value === null ||\n valueInCents === null ||\n referenceValueInCents === null\n ) {\n console.error(\n '[payment.placeOrder] the opened transaction payload is incomplete',\n missingTransactionFields(payload).join(', '),\n );\n return null;\n }\n\n return {\n gatewayUrl: payload.gatewayUrl,\n transactionId,\n merchantName,\n orderGroup,\n userProfileId,\n value,\n valueInCents,\n referenceValueInCents,\n };\n}\n\n/**\n * The one derived amount on the descriptor. VTEX's `payments[0]` carries no per-installment minor-unit\n * value, so T18 could not expose one and the conversion stays here — rounded, because\n * `19.99 * 100 === 1998.9999999999998`. `Number()` is not redundant: the components codegen maps the\n * `Decimal` scalar to `number` while the resolver serializes it as a string (see the spec's Open items).\n */\nfunction toMinorUnits(amount: number): number {\n return Math.round(Number(amount) * 100);\n}\n\n/**\n * The one call into holder-owned code. A holder's continuation is specified to resolve `userErrors` —\n * `sendPayments` already collapses every gateway outcome to one — so a throw means its contract broke.\n * Only a sanitised error *name* is logged (T160): the closure that threw holds the card, and a message\n * or stack could echo it — and so, unsanitised, could `.name` itself, since it is a writable own\n * property. See `loggableErrorName()`.\n */\nasync function invokeSubmission(\n submit: PaymentSubmission,\n transaction: PaymentGatewayTransaction,\n): Promise<readonly PaymentUserError[]> {\n try {\n // T160: totals the awaited resolution inside this `try`, mirroring `invokeStartTransaction`'s and\n // `invokeGatewayCallback`'s own T156 fix shape — a bare `return await submit(transaction)` let a\n // nullish resolution escape untouched, so the caller's own `submissionErrors.length` read threw\n // one call site away, latching the attempt at `opening` past the point `resume()` can reach it.\n const result = await submit(transaction);\n if (result == null) {\n console.error('[payment.placeOrder] the registered submission resolved with no result');\n return [NETWORK_ERROR];\n }\n return result;\n } catch (error) {\n console.error('[payment.placeOrder] the registered submission threw', loggableErrorName(error));\n return [NETWORK_ERROR];\n }\n}\n\n/**\n * The pre-flight counterpart of `invokeSubmission`, and it inherits that function's one rule: the\n * closure that throws holds the card, so only a sanitised error *name* is logged (T160) — never a\n * message, a stack, or the raw writable `.name` property. See `loggableErrorName()`.\n */\nfunction runPreflight(check: PaymentPreflightCheck): readonly PaymentUserError[] {\n try {\n return check();\n } catch (error) {\n console.error('[payment.placeOrder] the registered pre-flight threw', loggableErrorName(error));\n return [NETWORK_ERROR];\n }\n}\n\n/**\n * The `startTransaction` counterpart of `invokeSubmission`/`runPreflight` (fix pass, Medium finding):\n * before this, `run()` awaited `startTransaction()` directly, so a **rejection** — as opposed to a\n * resolved payload carrying `userErrors` — escaped past every one of `run()`'s own outcome branches\n * and `settleSequence`'s rejection handler never touches the attempt record, leaving it stuck at\n * `opening` for the payment root's lifetime. `opening` is the one phase `placeOrder()` treats as\n * \"already running\" with no failure exit, so every later click resolves `{ orderId: null, userErrors:\n * [] }` without running anything — a false \"Order placed successfully.\" with the CTA disabled forever.\n * `usePlaceOrder` is exported public API and `$actions` is compiler-injected and pluggable, so a\n * provider that rejects here is reachable, not hypothetical. Collapsing the rejection to a resolved\n * payload with one `userErrors` entry means `run()`'s own existing `payload.userErrors.length > 0`\n * branch is what catches it — the same `refused` outcome a server-reported refusal already gets,\n * correct here too since nothing can have been paid before the transaction even opened.\n *\n * T156 fold: the result is destructured inside this `try`, not returned as `await startTransaction()`\n * untouched — a provider resolving `undefined` used to throw at the caller's own\n * `payload.userErrors.length` read, past this function's own `catch`, latching the attempt at\n * `opening` exactly like the rejection case above. Destructuring a nullish resolution throws here\n * instead, so it collapses the same way. Mirrors `invokeGatewayCallback`'s own reason for\n * destructuring, below.\n *\n * Fix pass (review F1/S1): `userErrors` is defaulted with `?? [NETWORK_ERROR]`, not `?? []` — a\n * *partial* payload (present, but missing `userErrors`, e.g. a provider whose GraphQL selection omits\n * the field) used to leave it `undefined`, reaching the caller's `payload.userErrors.length` read\n * (`:627`) and throwing past this function's own `catch`, latching the attempt at `opening` exactly\n * like the cases already fixed. `userErrors` is this result's only signal of whether anything actually\n * went wrong, so a payload that omits it defaults to a *failure*, mirroring a rejection — defaulting to\n * `[]` instead would settle the attempt on a payload this function never actually saw. (The in-repo\n * default provider's own `payload?.userErrors ?? []`, `use-payment-actions.ts:202`, defaults to `[]`\n * only because there `payload` itself — not just the field — is what may be missing.)\n */\nasync function invokeStartTransaction(\n startTransaction: PaymentActions['startTransaction'],\n): Promise<PaymentStartTransactionResult> {\n try {\n const {\n transactionId,\n merchantName,\n gatewayUrl,\n orderGroup,\n userProfileId,\n value,\n valueInCents,\n referenceValueInCents,\n userErrors,\n } = await startTransaction();\n if (userErrors == null) {\n console.error('[payment.placeOrder] startTransaction resolved with no userErrors', orderGroup);\n }\n return {\n transactionId,\n merchantName,\n gatewayUrl,\n orderGroup,\n userProfileId,\n value,\n valueInCents,\n referenceValueInCents,\n userErrors: userErrors ?? [NETWORK_ERROR],\n };\n } catch (error) {\n console.error('[payment.placeOrder] startTransaction threw', loggableErrorName(error));\n return {\n transactionId: null,\n merchantName: null,\n gatewayUrl: null,\n orderGroup: null,\n userProfileId: null,\n value: null,\n valueInCents: null,\n referenceValueInCents: null,\n userErrors: [NETWORK_ERROR],\n };\n }\n}\n\n/**\n * The `gatewayCallback` counterpart of `invokeStartTransaction` above (T156, PR #756 review R-004):\n * before this, both call sites below awaited `gatewayCallback` directly, so a **rejection**\n * escaped past every outcome branch and `settleSequence`'s rejection handler never touches the attempt\n * record, leaving it stuck at `finalizing` for the payment root's lifetime — `placeOrder()` treats\n * `finalizing` exactly like `opening`, an already-running phase with no failure exit, so every later\n * click resolved `{ orderId: attempt.orderId, userErrors: [] }` without issuing anything.\n *\n * The destination is `unresolved`, never `refused` like `invokeStartTransaction`'s own collapse: this\n * leg only runs once the card already reached the gateway, so nothing here can pretend no money moved.\n * `unresolved` is exactly the phase `PlaceOrderAttemptPhase` already documents for this case, below,\n * and is already resumable.\n *\n * Three failure shapes collapse to the same `NETWORK_ERROR` result, logged distinctly (fix pass,\n * review F1/S1/F3): a **rejection**, caught below; a **nullish resolution** (`undefined`/`null`),\n * caught by the explicit check that follows rather than by letting a destructure throw, so it logs a\n * line of its own instead of printing identically to a rejection; and a **partial resolution** — an\n * object present but missing `userErrors`, e.g. a provider whose GraphQL selection omits the field —\n * defaulted with `?? [NETWORK_ERROR]`, not `?? []`. That last case used to return `{ orderId,\n * userErrors }` untouched, leaving `userErrors: undefined`, so the *caller's* `callback.userErrors.length`\n * read threw one call site away from where this wrapper's whole job is to stop exactly that. `[]` would\n * default a payload this function never actually saw to a clean success; `userErrors` is the only\n * signal of whether the callback settled anything, so omitting it means treating it as a failure.\n * Both failure branches log `orderGroup` and `attemptId` — neither is card data\n * (`missingTransactionFields`'s comment above), and this is the one point in the flow where money may\n * already have moved — but never the error's message, stack, or any call argument (FR-6).\n *\n * No `gatewayCallback` call may exist in this module outside this function (INV-2, asserted against\n * this file's own source text in use-place-order.test.tsx) — the two call sites below (`resume()` and\n * `run()`) call this and nothing else.\n */\nasync function invokeGatewayCallback(\n gatewayCallback: PaymentActions['gatewayCallback'],\n orderGroup: string,\n attemptId: string,\n): Promise<PaymentGatewayCallbackResult> {\n let result: PaymentGatewayCallbackResult | null | undefined;\n try {\n result = await gatewayCallback(orderGroup);\n } catch (error) {\n console.error('[payment.placeOrder] gatewayCallback threw', orderGroup, attemptId, loggableErrorName(error));\n return { orderId: null, userErrors: [NETWORK_ERROR] };\n }\n\n if (result == null) {\n console.error('[payment.placeOrder] gatewayCallback resolved with no result', orderGroup, attemptId);\n return { orderId: null, userErrors: [NETWORK_ERROR] };\n }\n\n if (result.userErrors == null) {\n console.error('[payment.placeOrder] gatewayCallback resolved with no userErrors', orderGroup, attemptId);\n }\n return { orderId: result.orderId ?? null, userErrors: result.userErrors ?? [NETWORK_ERROR] };\n}\n\n/**\n * T176 (FR-2, FR-3): what the caller reads from a `gatewayCallback` that reported `userErrors`, on both\n * paths that issue it. The gateway already accepted the card POST by then, so a result made only of this\n * package's own collapse — `PAYMENT_NETWORK_ERROR_CODE`, which `usePaymentActions` and\n * `invokeGatewayCallback` resolve for every thrown, lost or shapeless response — says nothing about\n * whether VTEX processed the order. It becomes PLACE_ORDER_UNCONFIRMED rather than a \"try again\" that\n * invites a second payment. A code the server sent is an answer, and comes back unchanged (INV-10).\n *\n * Total, and never throws: a non-array, or an entry with no readable string `code`, carries no server\n * code, so it reads as unconfirmed too. A server code elsewhere in the same list still wins, whatever\n * order the entries come in.\n */\nfunction reconcileCallbackUserErrors(userErrors: readonly PaymentUserError[]): readonly PaymentUserError[] {\n try {\n if (!Array.isArray(userErrors)) return [PLACE_ORDER_UNCONFIRMED_ERROR];\n if (userErrors.length === 0) return userErrors;\n const carriesServerCode = userErrors.some((userError) => {\n try {\n const code = userError.code;\n return typeof code === 'string' && code !== PAYMENT_NETWORK_ERROR_CODE;\n } catch {\n return false;\n }\n });\n return carriesServerCode ? userErrors : [PLACE_ORDER_UNCONFIRMED_ERROR];\n } catch {\n return [PLACE_ORDER_UNCONFIRMED_ERROR];\n }\n}\n\n/**\n * `crypto.randomUUID()` is secure-context-only (fix pass, Low finding) — on a plain-HTTP preview or\n * staging origin the method does not exist on `crypto` at all, so calling it throws, and every click\n * collapses to a generic `NETWORK_ERROR` with no diagnostic. `localhost` is always a secure context,\n * so dev never reproduces it. Falls back to a timestamp+random string, which only needs to be unique\n * for one payment root's lifetime (the staleness guard below is its one reader) — not a real UUID.\n */\nfunction generateAttemptId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Fix pass (High finding, race #2): guards every write past an attempt's genesis against a stale,\n * out-of-order completion clobbering a newer outcome. `run()`/`resume()` mint one write *before* their\n * own first await (safe by construction — nothing can have superseded an attempt this call itself just\n * created) and one or more writes *after* an await, once `startTransaction`/the continuation/\n * `gatewayCallback` resolve. Because the writing closure keeps running after its own CTA unmounts\n * (NFR-3), that later write cannot assume the shared attempt is still the one it started with — a\n * remounted or sibling instance may have already resumed it to a newer phase in the meantime. Using\n * the setter's own functional-update form (`SetPlaceOrderAttempt`) checks against whatever the shared\n * attempt actually is *when the write is applied*, not the value this closure captured before its\n * await — mirrors `selectionToken`'s own precedent (`matter/payment/use-payment-method-selection.ts`)\n * for the identical shape of problem, keyed on the attempt's own id instead of a second counter.\n * `settled` is additionally excluded because INV-6 forbids any transition away from it.\n */\nfunction writeAttemptIfCurrent(\n setAttempt: SetPlaceOrderAttempt | null | undefined,\n attemptId: string,\n next: PlaceOrderAttempt | null,\n): void {\n setAttempt?.((prev) => (prev !== null && prev.attemptId === attemptId && prev.phase !== 'settled' ? next : prev));\n}\n\n/**\n * T29 (Decision 3, FR-3): the only path back into a `sent`/`unresolved` attempt — `gatewayCallback` is\n * the sole call it ever issues. Module scope, not a `useCallback` inside the hook (fix pass, Low\n * finding): the previous version of this doc comment claimed `startTransaction`, the submission\n * channel, and `PaymentGatewayTransaction` were \"not in scope,\" so a careless future edit could not\n * make a resume re-pay — true at runtime today (verified by identifier sweep) but false as a\n * structural guarantee, since all three were merely unreferenced identifiers a nested closure could\n * still have read. Hoisting here makes the claim actually true: none of those three is even\n * *importable* from a bare module-level function that takes only `gatewayCallback`/`setAttempt`/\n * `setPlacing`/`current` as arguments — a resume is safe by construction, not by convention.\n */\nasync function resume(\n gatewayCallback: PaymentActions['gatewayCallback'] | undefined,\n setAttempt: SetPlaceOrderAttempt | null | undefined,\n setPlacing: (placing: boolean) => void,\n current: PlaceOrderAttempt,\n): Promise<PlaceOrderResult> {\n if (!gatewayCallback || current.orderGroup === null) {\n // Not reachable through this hook's own writes — §3's own invariant keeps `orderGroup` non-null\n // past `opening` — only through a caller-constructed attempt or a missing action namespace. Fails\n // the same way `run()`'s own first guard does, above.\n console.error('[payment.placeOrder] cannot resume an attempt with no order group');\n return { orderId: null, userErrors: [NETWORK_ERROR] };\n }\n\n // Mirrors `run()`'s own `setPlacing(true)` placement, immediately before its one `await`, so the\n // shared placing state stays accurate across a remount too. A plain write, guarded by\n // `writeAttemptIfCurrent` rather than an unconditional one, since `current` is a caller-supplied\n // snapshot that may already be stale by the time this async function actually runs.\n writeAttemptIfCurrent(setAttempt, current.attemptId, { ...current, phase: 'finalizing' });\n setPlacing(true);\n\n const callback = await invokeGatewayCallback(gatewayCallback, current.orderGroup, current.attemptId);\n const failed = callback.userErrors.length > 0;\n // T176 (FR-4, FR-7): the result is fixed by the callback before the write, so a caller-supplied\n // writer that throws can no longer turn a confirmed order into a rejection — `settleSequence` would\n // resolve that as NETWORK_ERROR. The record may stay at `finalizing` then; nothing here can repair a\n // broken writer. Only a sanitised error name is logged, as on every other catch in this module.\n const result: PlaceOrderResult = failed\n ? { orderId: null, userErrors: reconcileCallbackUserErrors(callback.userErrors) }\n : { orderId: callback.orderId, userErrors: [] };\n\n try {\n writeAttemptIfCurrent(\n setAttempt,\n current.attemptId,\n failed ? { ...current, phase: 'unresolved' } : { ...current, phase: 'settled', orderId: callback.orderId },\n );\n } catch (error) {\n console.error('[payment.placeOrder] the resume record write threw', current.attemptId, loggableErrorName(error));\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// The order-attempt record (T29, specs/make-an-order-attempt-survive-a.md) — an identity and a phase\n// that outlive the CTA's own mount, so a remount or a second click resumes a sequence instead of\n// re-running its dangerous half. Defined here, not in matter/payment/use-payment-method-selection.ts:\n// this is the only module that knows the sequence is three calls, and the layering stays\n// matter → sdk (submission-registry.tsx's own rule) — usePaymentMethodSelection imports this type,\n// mirroring `PaymentPath`'s own alias of `PaymentSubmissionPath`.\n// ---------------------------------------------------------------------------\n\n/**\n * How far an order attempt got. The phase, not an error code, is what says whether money may already\n * have moved — an aborted card POST and a refused transaction both surface as the same userError, and\n * only this distinguishes them.\n */\nexport type PlaceOrderAttemptPhase =\n /** `startTransaction` issued or resolved. Nothing has been paid. */\n | 'opening'\n /** A leg refused before the card left the browser. A fresh full attempt is legal. */\n | 'refused'\n /** The continuation resolved with no userErrors — the card reached the gateway. */\n | 'sent'\n /** The card's fate is unknown: the continuation threw, or the POST was aborted, or a finalization\n * did not resolve. Treated exactly like `sent`. */\n | 'unresolved'\n /** `gatewayCallback` is in flight for this attempt's order group. */\n | 'finalizing'\n /** `gatewayCallback` resolved with no userErrors. Terminal. */\n | 'settled';\n\nexport type PlaceOrderAttempt = Readonly<{\n /** Identity that outlives any one mount. Not `useId()` — that is per-instance and resets. */\n attemptId: string;\n phase: PlaceOrderAttemptPhase;\n /** Resolved by `startTransaction`; the only key a resume needs. Null before `opening` settles. */\n orderGroup: string | null;\n /**\n * Resolved by `gatewayCallback`. Not an independent identifier: the resolver returns\n * `orderId: input.orderGroup` (`checkout/index.ts:1965`), echoing the argument back, because the\n * upstream success response body is discarded. So `orderId === orderGroup` on every success today.\n */\n orderId: string | null;\n}>;\n\n/**\n * Fix pass (High finding, race #2): widened past a plain value so a write can be made conditional on\n * whatever the shared attempt actually is *at write time* — the primitive `useState` already exposes\n * this (its setter has always accepted an updater function; only this package's own declared type\n * stopped a caller from using that form). A plain value remains valid; every existing caller that\n * never needed the guard is unaffected.\n */\nexport type SetPlaceOrderAttempt = (\n update: PlaceOrderAttempt | null | ((prev: PlaceOrderAttempt | null) => PlaceOrderAttempt | null),\n) => void;\n\n// ---------------------------------------------------------------------------\n// The hook\n// ---------------------------------------------------------------------------\n\nexport type UsePlaceOrderArgs = {\n /** `Payment` slice from the caller's fragment. */\n $data?: PlaceOrderData;\n /** Compiler-injected action surface; carries `payment.startTransaction`, `payment.gatewayCallback`, and `analytics.track`. */\n $actions?: ComponentActions;\n /**\n * T29's order-attempt record and its writer — `usePayment().attempt` / `.setAttempt`, forwarded by\n * the caller exactly like `billingAddressDifferent` below. **Optional by necessity, not by safety**\n * (mirrors `submitGate`'s own doc note): a caller that omits either gets today's behaviour, silently\n * — `placeOrder()` always takes the full `run()` path, with no resume and no phase written anywhere.\n * `PlaceOrder` always sources both from `usePayment()`.\n */\n attempt?: PlaceOrderAttempt | null;\n /** See `attempt` above. Identity must be stable across renders (NFR-3) — `usePayment().setAttempt` already is. */\n setAttempt?: SetPlaceOrderAttempt | null;\n /**\n * `usePayment().draft.billingAddressDifferent` (T11's widening of `PaymentDraft`) — an explicit\n * argument, not read from `usePayment()`'s context in this hook, so the hook keeps its existing\n * shape of taking every fact it needs as a parameter rather than adding a second hard context\n * dependency alongside the submission channel below. `null`/omitted folds to the same \"not set\"\n * descriptor shape `gateway.ts`'s own `transaction.billingAddressDifferent ?? false` already expects\n * (fix pass, PR #544 review F1). The caller is responsible for sourcing this from `usePayment()` and\n * forwarding it here.\n */\n billingAddressDifferent?: boolean | null;\n /**\n * A client-side refusal the caller already resolved, or `null` to proceed. Deliberately an opaque\n * `PaymentUserError` rather than the facts behind it: this module may name no identifier matching\n * `CARD_FIELD_PATTERN`, which includes `document`. The caller owns resolving it —\n * `matter/payment/usePaymentSubmitGate()` is the one in this package.\n *\n * **Optional by necessity, not by safety.** This hook is exported public API; `PlaceOrder` is the\n * only in-repo caller, and it always sources this from `usePaymentSubmitGate()`, but nothing stops a\n * storefront from composing its own place-order button directly on this hook. Omitting this argument\n * is silent and behaves exactly as if this whole pre-flight branch did not exist — no cardholder or\n * billing-address refusal ever reaches a CTA built that way, even though the same client-side gate\n * would have caught it. There is no compile-time signal for that omission (making the type mandatory\n * would be a breaking change to a hook other code may already call, and a caller can still opt out\n * explicitly with `null` regardless of the type), so a store composing its own CTA on this hook must\n * source this argument from `usePaymentSubmitGate()` deliberately, not by copying an example that\n * happened to include it.\n */\n submitGate?: PaymentUserError | null;\n};\n\nexport function usePlaceOrder({\n $data,\n $actions,\n attempt,\n setAttempt,\n billingAddressDifferent,\n submitGate,\n}: UsePlaceOrderArgs): UsePlaceOrder {\n const { path, resolve, resolvePreflight } = usePaymentSubmission();\n\n const [placing, setPlacing] = useState(false);\n const [userErrors, setUserErrors] = useState<readonly PaymentUserError[]>([]);\n const [orderId, setOrderId] = useState<string | null>(null);\n const inFlight = useRef<Promise<PlaceOrderResult> | null>(null);\n\n const startTransaction = $actions?.payment?.startTransaction;\n const gatewayCallback = $actions?.payment?.gatewayCallback;\n const track = $actions?.analytics?.track;\n\n const selectedMethodId = $data?.selectedMethodId ?? null;\n const methods = $data?.methods;\n const selectedInstallment = $data?.selectedInstallment ?? null;\n\n const selectedMethod = useMemo(\n () => methods?.find((method) => method.id === selectedMethodId) ?? null,\n [methods, selectedMethodId],\n );\n\n const run = useCallback(async (): Promise<PlaceOrderResult> => {\n if (!startTransaction || !gatewayCallback) {\n console.error('[payment.placeOrder] the payment action namespace is not injected');\n return { orderId: null, userErrors: [NETWORK_ERROR] };\n }\n\n // Resolved before anything is opened (epic, T13): a transaction nothing can complete is dangling\n // state on the order form. The two causes are one state to a shopper and differ only in the log.\n const submit = path === null ? null : resolve(path);\n if (submit === null) {\n console.error('[payment.placeOrder] no submission is registered for the entry path', path ?? 'none');\n return { orderId: null, userErrors: [MISSING_SUBMISSION_ERROR] };\n }\n\n // `paymentSystem` and `groupName` come from the read model, not from the start payload, so a\n // missing selection would otherwise surface only after the transaction was already open.\n if (selectedMethod === null) {\n console.error('[payment.placeOrder] no payment method is selected');\n return { orderId: null, userErrors: [MISSING_METHOD_ERROR] };\n }\n\n // T24's submit gate: an already-resolved client-side refusal (`matter/payment/\n // usePaymentSubmitGate()`), joining the pre-flight so no transaction opens for a submission this\n // client already knows would be refused. Only the opaque verdict is logged — never a fact behind\n // it — keeping this module's own PCI boundary intact (Decision 2).\n if (submitGate) {\n console.error('[payment.placeOrder] a client-side gate refused the submission', submitGate.code);\n return { orderId: null, userErrors: [submitGate] };\n }\n\n // The holder's own pre-flight on the draft it holds (T27). It resolves the same verdict its\n // submission would have resolved at :348, one network leg earlier, so an avoidable input mistake\n // never opens a transaction that is then left dangling. Only the opaque refusal crosses back —\n // this module learns nothing about what was wrong, exactly as with the gate above.\n //\n // Absence is a silent no-op, not a refusal (FR-6) — the same choice `submitGate` two branches\n // above makes on its own absence, staying quiet when it is simply falsy too. The parallel stops\n // there: `submitGate`'s truthiness *is* the refusal, while `preflight`'s only means a check was\n // registered. Deliberate, not overlooked (fix pass, review S1): a holder that registered no check\n // (the two-argument overload) contributes nothing here, exactly as if this branch did not exist.\n // Logging on this branch would fire on the overwhelming majority of calls — it is every pre-T27\n // holder's default, and most of this hook's own test suite exercises it as the baseline — diluting\n // the signal this file's other console.error calls carry for genuine anomalies. Failing closed\n // instead would refuse every one of those callers, breaking `register`'s own optional third\n // argument (NFR-6, Decision 2). A caller that wants this protection must register a check.\n const preflight = path === null ? null : resolvePreflight(path);\n if (preflight) {\n const refusals = runPreflight(preflight);\n if (refusals.length > 0) {\n console.error('[payment.placeOrder] the registered pre-flight refused the submission', refusals[0]?.code);\n return { orderId: null, userErrors: refusals };\n }\n }\n\n // T170 (FR-2/FR-3): the three facts this function's own `catch` needs and cannot read from inside\n // its `try`. `handedOver` records whether the card had already been handed to the registered\n // submission when a throw happened — the record's own phase cannot answer that, since it reads\n // `opening` on both sides of the order-group write below. `settledOrderGroup` carries the one\n // field an `unresolved` record must keep for `resume()` to be able to act on it at all, since the\n // settled payload itself is scoped inside the `try`.\n //\n // `placedAttempt` is the terminal record the success tail below is about to write, captured\n // *before* that write (fix round, Critical finding). `handedOver` cannot stand in for it: it is\n // equally true for the whole span from the card POST onwards, including the window *after*\n // `gatewayCallback` has already resolved a real order — and in that window neither `refused` nor\n // `unresolved` is the truth, because the order is placed. The sequence is the only thing that\n // knows it got there, so it carries the fact rather than letting the `catch` infer a phase the\n // record cannot distinguish.\n //\n // T176 (FR-5, Decision 4): `outcome` is the result each branch past the card hand-over fixes\n // *before* its own record write, so the `catch` can return it. From that point a throw — by a\n // caller-supplied writer or by the tracker — may still need the record recovered, but it no longer\n // changes what the caller reads: a confirmed order stays confirmed, and a refusal stays that refusal.\n // It is still null for a throw raised before any branch fixed a result, which keeps T170's\n // NETWORK_ERROR.\n //\n // Locals, not fields on `PlaceOrderAttempt` (NFR-1): only this closure ever reads them, and\n // putting any of them on the exported record would re-open the question T167 closed — readers\n // second-guessing the phase (INV-4).\n let handedOver = false;\n let settledOrderGroup: string | null = null;\n let placedAttempt: PlaceOrderAttempt | null = null;\n let outcome: PlaceOrderResult | null = null;\n\n // T29 (FR-1/FR-2): a fresh attempt record, minted only once every synchronous refusal guard above\n // has passed — none of those guards ever creates one, so a pre-card refusal (Scenario 6) leaves\n // `attempt` untouched and a fresh full run stays legal next time. Not `useId()` (T28's own\n // `instanceKey`): that resets on every remount, and this identity must not. A plain write, not\n // `writeAttemptIfCurrent`: this is the genesis of the attempt, so there is nothing yet for a\n // concurrent completion to have superseded.\n const attemptId = generateAttemptId();\n setAttempt?.({ attemptId, phase: 'opening', orderGroup: null, orderId: null });\n try {\n // FR-1 (T56): raised here — after every synchronous refusal guard, immediately before the first\n // `await` — so a refusal that returns before this line never commits a `placing: true` frame.\n // `setUserErrors([])` and `setOrderId(null)` remain in `placeOrder` (the click handler), and\n // `settle`'s `setPlacing(false)` is untouched.\n setPlacing(true);\n\n const payload = await invokeStartTransaction(startTransaction);\n\n // Before the completeness check, so a server diagnosis is never replaced by a synthesized one —\n // T18 returns INVALID_TRANSACTION_DATA with every other field null for a transaction VTEX opened\n // with no merchant to pay, which a null-field check would otherwise swallow.\n if (payload.userErrors.length > 0) {\n // T29 (Decision 4, Scenario 6): nothing was paid, so the attempt records `refused` — a fresh\n // full sequence stays legal next time (`placeOrder()`'s own branch falls through to `run()` for\n // any phase other than opening/finalizing/settled/sent/unresolved, `refused` included). Fix\n // pass (Critical finding): this used to reset straight to `null`, which is indistinguishable\n // from \"no attempt ever started\" to a *different*, still-mounted or freshly-remounted instance —\n // `usePaymentMethodSelection`'s shared `placing` derivation reads this phase specifically to\n // release a lock a CTA that unmounted mid-flight can never release itself (FR-5, INV-5).\n // `writeAttemptIfCurrent`, not a plain write: this closure may be running after its own CTA\n // unmounted (NFR-3), so a newer completion for this same attempt must win instead.\n writeAttemptIfCurrent(setAttempt, attemptId, { attemptId, phase: 'refused', orderGroup: null, orderId: null });\n return { orderId: null, userErrors: payload.userErrors };\n }\n\n const settled = settleTransaction(payload);\n if (settled === null) {\n writeAttemptIfCurrent(setAttempt, attemptId, { attemptId, phase: 'refused', orderGroup: null, orderId: null });\n return { orderId: null, userErrors: [INVALID_TRANSACTION_ERROR] };\n }\n\n // T167: orderGroup is known and validated as soon as settleTransaction() returns non-null — well\n // before invokeSubmission() (the card POST, below) resolves. Writing it now closes that gap for the\n // whole 'opening' window instead of leaving it null until a terminal write. Data-completeness only:\n // placeOrder()'s phase branch and resumable (place-order.tsx) still derive only from `phase`, which\n // stays 'opening' here, so neither becomes actionable while this record sits in this phase.\n //\n // T170: kept for the recovery path too, which cannot reach `settled` from the `catch` below.\n settledOrderGroup = settled.orderGroup;\n writeAttemptIfCurrent(setAttempt, attemptId, {\n attemptId,\n phase: 'opening',\n orderGroup: settled.orderGroup,\n orderId: null,\n });\n\n const transaction: PaymentGatewayTransaction = {\n gatewayUrl: settled.gatewayUrl,\n transactionId: settled.transactionId,\n merchantName: settled.merchantName,\n orderGroup: settled.orderGroup,\n userProfileId: settled.userProfileId,\n paymentSystem: selectedMethod.id,\n groupName: selectedMethod.groupName,\n currencyCode: settled.value.currencyCode,\n valueInCents: settled.valueInCents,\n referenceValueInCents: settled.referenceValueInCents,\n installments: selectedInstallment?.count ?? 1,\n // A count: 1 installment is the full transaction value by definition. The read model's\n // selectedInstallment.value for a synthesized 1x tier is option.value (mappers.ts:597),\n // which can differ from the transaction's own value (Decision 8, spec:232) — e.g. an option\n // priced at 10000 on an order form whose value is 8997. Using settled.valueInCents for the\n // 1x case avoids charging option.value instead of the transaction value. For count > 1 the\n // tier value is upstream data, never synthesized, so the per-installment amount is correct.\n installmentsValueInCents:\n selectedInstallment && selectedInstallment.count !== 1\n ? toMinorUnits(selectedInstallment.value.amount)\n : settled.valueInCents,\n interestRate: selectedInstallment?.interestRate ?? 0,\n // Omitted entirely (never an explicit `undefined` key) when the caller passed none, matching\n // `callbackUrl`'s own absent-when-not-provided convention above (fix pass, review F1).\n ...(billingAddressDifferent != null ? { billingAddressDifferent } : {}),\n };\n\n // T170 (FR-3): raised on this side of the await, never the other. From the next line on, the\n // card has left the browser and nothing may recover this attempt as `refused` — which is the\n // only fact the `catch` below cannot re-derive, since the record reads `opening` either way.\n handedOver = true;\n const submissionErrors = await invokeSubmission(submit, transaction);\n // An unpaid transaction is never finalized.\n if (submissionErrors.length > 0) {\n // T29 (FR-2, Decision 5): the card's fate is unknown past this point. `gateway.ts` resolves one\n // opaque userError whether the gateway declined the card, the network failed, or the POST was\n // aborted (FR-6) — this hook cannot and must not try to tell those apart, so every one of them is\n // `unresolved`, never `refused`. Sticky for the payment root's lifetime (FR-4): the only call\n // `resume()` ever issues again is `gatewayCallback(orderGroup)`.\n outcome = { orderId: null, userErrors: submissionErrors };\n writeAttemptIfCurrent(setAttempt, attemptId, {\n attemptId,\n phase: 'unresolved',\n orderGroup: settled.orderGroup,\n orderId: null,\n });\n return outcome;\n }\n\n // T29: the card reached the gateway clean. Past this line the card may be charged — `resume()`\n // (below) is the only legal path back in (Decision 3). Fix pass (High finding): written as\n // `finalizing`, not `sent` — `sent` and `finalizing` are two distinct edges in the spec's own state\n // diagram (`sent --> finalizing: gatewayCallback issued`), but this call issues `gatewayCallback`\n // with no `await` in between, so nothing can ever legally observe the record at rest in `sent`\n // here: React batches same-tick writes, so whichever value is written *last*, before the next\n // `await`, is the only one any render (or a remount reading mid-flight) ever sees. The previous\n // version of this comment argued that fact meant the choice between the two values did not matter;\n // it does — `resumable` (place-order.tsx) treats `sent` as safe to resume, so writing it here left\n // `resumable` true for the entire span of this in-flight `gatewayCallback`, and a remounted or\n // sibling CTA could issue a second, concurrent one on the same order group (this file's own \"is\n // between the card POST and gatewayCallback\" test used to pin exactly that window). `finalizing` is\n // the value that keeps `resumable` false until this call actually settles — exactly what `resume()`\n // (below) already writes for its own, separately-triggered invocation of this same call.\n writeAttemptIfCurrent(setAttempt, attemptId, {\n attemptId,\n phase: 'finalizing',\n orderGroup: settled.orderGroup,\n orderId: null,\n });\n\n const callback = await invokeGatewayCallback(gatewayCallback, settled.orderGroup, attemptId);\n if (callback.userErrors.length > 0) {\n outcome = { orderId: null, userErrors: reconcileCallbackUserErrors(callback.userErrors) };\n writeAttemptIfCurrent(setAttempt, attemptId, {\n attemptId,\n phase: 'unresolved',\n orderGroup: settled.orderGroup,\n orderId: null,\n });\n return outcome;\n }\n\n // Built into a local before it is written, not inline (fix round, Critical finding): the write\n // below is itself a throw site — `setAttempt` is caller-supplied — and from the line above onwards\n // the order is placed. Handing the same value to the `catch` lets it re-assert the terminal truth\n // instead of downgrading a paid order to a resumable one.\n placedAttempt = {\n attemptId,\n phase: 'settled',\n orderGroup: settled.orderGroup,\n orderId: callback.orderId,\n };\n outcome = { orderId: callback.orderId, userErrors: [] };\n\n // GA4's transaction_id is the order a merchant reconciles against. A callback can succeed without\n // resolving one, and that is still a placed order, so the payment transaction id is the fallback.\n // `settled.value.amount` is typed `number` but arrives over the wire as a decimal string (the same\n // Decimal-scalar mismatch `toMinorUnits` coerces above), so GA4's numeric `value` needs the same\n // `Number()` coercion the rest of this package applies at the point of numeric use (e.g.\n // `sdk/cart/optimistic.ts`'s `moneyAdd`).\n //\n // T176 (Decision 5): dispatched before the terminal write, not after it. The event reports VTEX's\n // confirmation, which has already happened; the record is local bookkeeping about that fact, so a\n // writer that throws on it no longer loses the event. A tracker that throws lands in the `catch`\n // before that write, and the recovery write lands `placedAttempt` instead — never a second call.\n track?.({\n name: 'purchase',\n transactionId: callback.orderId ?? settled.transactionId,\n value: { amount: Number(settled.value.amount), currencyCode: settled.value.currencyCode },\n });\n\n writeAttemptIfCurrent(setAttempt, attemptId, placedAttempt);\n\n return outcome;\n } catch (error) {\n // T170 (FR-1): before this, `run()` had no `try`/`catch` of its own, so a throw anywhere past\n // the genesis write above — the synchronous descriptor build, or the caller-supplied `setAttempt`\n // that `writeAttemptIfCurrent` invokes — escaped every one of the outcome branches inside it.\n // `settleSequence`'s rejection handler (`placeOrder`, below) restores this hook's *local* state\n // only and never touches the shared record, so the record stayed latched at `opening` with no\n // sequence alive to move it: every later click hit `placeOrder()`'s own in-flight branch and\n // issued nothing, for the payment root's lifetime.\n //\n // The phase this settles on is not a judgement call, but it is a three-way one, and the first\n // version of this catch read it from `handedOver` alone (fix round, Critical finding). A throw\n // before the card was handed over provably never started the card POST, so nothing was paid and\n // `refused` is correct — a fresh full attempt stays legal (T29 Decision 4). A throw after it\n // with no order resolved means the card's fate is unknown, which is what `unresolved` means\n // (Decision 5) and what `resume()` exists for. But the `settled` write above is a throw site\n // too, and at that one site `gatewayCallback` has already returned a real order while `settled`\n // has not yet landed — so `writeAttemptIfCurrent`'s own `settled` guard does not refuse the\n // write either, and `unresolved` there made `resumable` (`place-order.tsx`) true for an order\n // that was already paid: one click on the re-enabled CTA routed through `resume()` into a second\n // `gatewayCallback` on the same order group, undeduplicated at every layer (no ownership check,\n // no idempotency key in `PaymentGatewayCallbackInput`, and VTEX's behaviour on a repeat carried\n // as unmeasured residual risk under PEXP-2049), doubling pressure on the account-wide\n // `vtex.checkout` breaker as well. Hence `placedAttempt` first: re-asserting the terminal record\n // the failed write was carrying is the only phase that is true there, and it can never be a\n // downgrade, because `writeAttemptIfCurrent` refuses it outright once `settled` has landed\n // (INV-6). Since T176 the `track?.()` call sits before that write, so for a tracker that throws\n // this recovery write is the one that lands `settled`.\n //\n // `writeAttemptIfCurrent`, never a plain write (INV-1): this closure may be running after its\n // own CTA unmounted (NFR-3), so a newer completion for this same attempt must win instead. That\n // write is itself wrapped, because it goes through the same caller-supplied `setAttempt` that may\n // be what threw in the first place (fix round, High finding): `writeAttemptIfCurrent` passes the\n // setter a *function* updater, so a writer that rejects that form — one that serializes,\n // schema-validates or persists what it is given — lands the plain genesis write and throws on\n // every write after it, this one included. Unguarded, that second throw escaped `run()` and left\n // this hook's contract (FR-6: resolve, never reject) resting on `settleSequence` catching it a\n // frame away. Nothing further can be tried once the writer itself is broken — a plain\n // `setAttempt` fallback is exactly the stale-closure clobber INV-1 forbids — so it is logged and\n // the result is returned either way: the `outcome` a branch already fixed (T176, FR-5), or\n // NETWORK_ERROR when the throw came before any branch fixed one. Only a sanitised error *name* is\n // logged, on both lines (FR-7): the closure that threw may hold the card, and a message, a stack,\n // or the raw writable `.name` could echo it. See `loggableErrorName()`.\n console.error('[payment.placeOrder] the place-order sequence threw', attemptId, loggableErrorName(error));\n try {\n writeAttemptIfCurrent(\n setAttempt,\n attemptId,\n placedAttempt ??\n (handedOver\n ? { attemptId, phase: 'unresolved', orderGroup: settledOrderGroup, orderId: null }\n : { attemptId, phase: 'refused', orderGroup: null, orderId: null }),\n );\n } catch (writeError) {\n console.error(\n '[payment.placeOrder] the place-order recovery write threw',\n attemptId,\n loggableErrorName(writeError),\n );\n }\n return outcome ?? { orderId: null, userErrors: [NETWORK_ERROR] };\n }\n }, [\n startTransaction,\n gatewayCallback,\n track,\n path,\n resolve,\n resolvePreflight,\n selectedMethod,\n selectedInstallment,\n billingAddressDifferent,\n submitGate,\n setAttempt,\n ]);\n\n const placeOrder = useCallback<UsePlaceOrder['placeOrder']>(() => {\n // A second click lands before T14's disabled state commits a render, and duplicating a\n // transaction is the worst outcome this surface has — so the guard is the in-flight promise\n // itself, not the `placing` flag. Both callers observe the one attempt that ran.\n if (inFlight.current !== null) return inFlight.current;\n\n // FR-1 (T56): `setPlacing(true)` moved into `run()`/`resume()`, immediately before their own first\n // `await`, so a synchronous refusal never commits a `placing: true` frame. `run()`'s and `resume()`'s\n // outcomes share this one settle wrapper (T29).\n const settleSequence = (sequence: Promise<PlaceOrderResult>): Promise<PlaceOrderResult> => {\n setUserErrors([]);\n setOrderId(null);\n\n const settle = (result: PlaceOrderResult): PlaceOrderResult => {\n inFlight.current = null;\n setPlacing(false);\n setUserErrors(result.userErrors);\n setOrderId(result.orderId);\n return result;\n };\n\n // `run()`/`resume()` resolve for every path this hook drives itself, but a collaborator outside\n // its control (a different $actions provider, or a mock resolving `undefined`) can still reject\n // one. Settling on both paths keeps `placing` from wedging true forever and surfaces an error, so\n // the CTA is clickable again either way. Spec correction (review F2): whether the *next* click\n // actually retries anything is decided by the attempt's phase, not by this settle —\n // `invokeStartTransaction`/`invokeGatewayCallback` each write a resumable phase before their own\n // await and normalize their result, so a throw on either leg lands on `refused`/`unresolved`\n // (T156). T170 closes the rest of that class at the source: `run()` now catches its own throws\n // and settles the record itself, so what is left is a collaborator-owned promise that never\n // settles at all — nothing has thrown, so there is nothing for either that catch or this settle\n // to act on, and bounding it needs a deadline nobody has set yet (T170's own OQ-1).\n const outcome = sequence.then(settle, () => settle({ orderId: null, userErrors: [NETWORK_ERROR] }));\n\n inFlight.current = outcome;\n return outcome;\n };\n\n // T29 (FR-2, FR-3): the CTA's next legal action is derived from the surviving attempt's own phase,\n // not always `run()`. `sent`/`unresolved` resume through a module-level function with no access at\n // all to `startTransaction` or the submission channel (Decision 3); `opening`/`finalizing` are\n // already running — possibly from a mount this closure has outlived (NFR-3) — and `settled` is\n // terminal (INV-6), so none of the three issues anything, independent of whatever a composing\n // CTA's own disabled state happens to do. Only the terminal one reads as a success, though: see\n // the in-flight arm's own comment below. `refused` falls through to a full run below like `null` —\n // nothing was paid, so a fresh attempt is legal (Decision 4). The `'sent'` check just below is\n // defensive-only: `run()` never leaves the record resting there today (it writes `finalizing`\n // directly, same tick — see that write site's own comment); kept so a future write path resting at\n // `sent` would still resume correctly instead of silently falling through to a full run.\n if (attempt) {\n if (attempt.phase === 'settled') {\n return Promise.resolve({ orderId: attempt.orderId, userErrors: [] });\n }\n // T170 (FR-5, Decision 4): split off the two in-flight phases, which used to share this arm and\n // so resolved the empty `userErrors` this hook's own contract defines as success\n // (`PlaceOrderResult`) — a reader that did not start the sequence was handed a success shape for\n // an order still in flight, and a CTA derived from it (`place-order.tsx`'s `succeeded`) rendered\n // \"placed\" for an order nobody had paid for. Settled through `settleSequence` like every other\n // outcome, so the refusal reaches this hook's own `userErrors` rather than only the resolved\n // value a composing CTA may never read. Still issues nothing, and still reads the phase alone\n // (INV-4): waiting is the correct behaviour while the card POST is genuinely open.\n if (attempt.phase === 'opening' || attempt.phase === 'finalizing') {\n return settleSequence(Promise.resolve({ orderId: attempt.orderId, userErrors: [PLACE_ORDER_IN_FLIGHT_ERROR] }));\n }\n if (attempt.phase === 'sent' || attempt.phase === 'unresolved') {\n return settleSequence(resume(gatewayCallback, setAttempt, setPlacing, attempt));\n }\n }\n\n return settleSequence(run());\n }, [run, attempt, gatewayCallback, setAttempt]);\n\n return useMemo(() => ({ placeOrder, placing, userErrors, orderId }), [placeOrder, placing, userErrors, orderId]);\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { defineActions } from "../../core/dist/actions/registry.js";
|
|
3
3
|
import "../../core/dist/index.js";
|
|
4
|
-
import { SHIPPING_ADDRESS_UPDATE, SHIPPING_NETWORK_ERROR_CODE } from "./shipping-operations.js";
|
|
5
4
|
import { SHIPPING_OPTION_SELECT } from "./option-select-operations.js";
|
|
5
|
+
import { SHIPPING_ADDRESS_UPDATE, SHIPPING_NETWORK_ERROR_CODE } from "./shipping-operations.js";
|
|
6
6
|
import { useMutation } from "@apollo/client/react";
|
|
7
7
|
import { useCallback, useMemo } from "react";
|
|
8
8
|
//#region src/sdk/shipping/use-shipping-actions.ts
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/utils/loggable-error-name.ts
|
|
2
|
+
/**
|
|
3
|
+
* `Error.prototype.name` is a writable own property (security pass, T156 review; fixed here, T160):
|
|
4
|
+
* any collaborator a caller wraps in a `try`/`catch` — a holder's continuation, a pre-flight check,
|
|
5
|
+
* an action provider, a GraphQL mutation rejection — can throw an object whose `.name` is arbitrary
|
|
6
|
+
* text, including text built from card data or PII the caller exists to keep out of a log. An
|
|
7
|
+
* allow-list, not a pattern or a deny-list, mirrors this package's own rule for exactly this shape of
|
|
8
|
+
* problem (`matter/sensitive-field-protection.test.ts`'s own "an allow-list fails loudly ... a
|
|
9
|
+
* deny-list fails silently"). Lives in `utils/`, not in any one domain's `sdk/` subdirectory, because
|
|
10
|
+
* its callers already span two peer domains — `payment` and `identity` — neither of which should
|
|
11
|
+
* import a logging helper from the other (T160, Decision 6).
|
|
12
|
+
*/
|
|
13
|
+
const LOGGABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
14
|
+
"Error",
|
|
15
|
+
"EvalError",
|
|
16
|
+
"RangeError",
|
|
17
|
+
"ReferenceError",
|
|
18
|
+
"SyntaxError",
|
|
19
|
+
"TypeError",
|
|
20
|
+
"URIError",
|
|
21
|
+
"AggregateError"
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* The one place every caller below decides what to log for a caught error. Never the raw `.name`.
|
|
25
|
+
*
|
|
26
|
+
* `error` is an untrusted value: a holder's continuation, a pre-flight check, or any other
|
|
27
|
+
* pluggable collaborator can throw whatever object it wants, engineered so that merely inspecting
|
|
28
|
+
* it — not only reading `.name` — throws (fix rounds 1-3, all Medium findings). A `Proxy` can trap
|
|
29
|
+
* `getPrototypeOf` (triggered by the `instanceof` check below), `get` (triggered by the `.name`
|
|
30
|
+
* read), or make either one answer differently across repeated calls. Rather than guard each
|
|
31
|
+
* mechanism one at a time, every operation this function performs on `error` — the `instanceof`
|
|
32
|
+
* check, the `.name` read, and the allow-list membership check — runs inside one `try`/`catch`:
|
|
33
|
+
* whatever throws, and for whatever reason, collapses to `'UnknownError'` exactly like any other
|
|
34
|
+
* unrecognised name. `error.name` is still read exactly once, into `name` below, so a getter that
|
|
35
|
+
* answers differently across separate calls has no second read left to answer differently on.
|
|
36
|
+
*/
|
|
37
|
+
function loggableErrorName(error) {
|
|
38
|
+
try {
|
|
39
|
+
if (!(error instanceof Error)) return "UnknownError";
|
|
40
|
+
const name = error.name;
|
|
41
|
+
return LOGGABLE_ERROR_NAMES.has(name) ? name : "UnknownError";
|
|
42
|
+
} catch {
|
|
43
|
+
return "UnknownError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { loggableErrorName };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=loggable-error-name.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loggable-error-name.js","names":[],"sources":["../../src/utils/loggable-error-name.ts"],"sourcesContent":["/**\n * `Error.prototype.name` is a writable own property (security pass, T156 review; fixed here, T160):\n * any collaborator a caller wraps in a `try`/`catch` — a holder's continuation, a pre-flight check,\n * an action provider, a GraphQL mutation rejection — can throw an object whose `.name` is arbitrary\n * text, including text built from card data or PII the caller exists to keep out of a log. An\n * allow-list, not a pattern or a deny-list, mirrors this package's own rule for exactly this shape of\n * problem (`matter/sensitive-field-protection.test.ts`'s own \"an allow-list fails loudly ... a\n * deny-list fails silently\"). Lives in `utils/`, not in any one domain's `sdk/` subdirectory, because\n * its callers already span two peer domains — `payment` and `identity` — neither of which should\n * import a logging helper from the other (T160, Decision 6).\n */\nconst LOGGABLE_ERROR_NAMES: ReadonlySet<string> = new Set([\n 'Error',\n 'EvalError',\n 'RangeError',\n 'ReferenceError',\n 'SyntaxError',\n 'TypeError',\n 'URIError',\n 'AggregateError',\n]);\n\n/**\n * The one place every caller below decides what to log for a caught error. Never the raw `.name`.\n *\n * `error` is an untrusted value: a holder's continuation, a pre-flight check, or any other\n * pluggable collaborator can throw whatever object it wants, engineered so that merely inspecting\n * it — not only reading `.name` — throws (fix rounds 1-3, all Medium findings). A `Proxy` can trap\n * `getPrototypeOf` (triggered by the `instanceof` check below), `get` (triggered by the `.name`\n * read), or make either one answer differently across repeated calls. Rather than guard each\n * mechanism one at a time, every operation this function performs on `error` — the `instanceof`\n * check, the `.name` read, and the allow-list membership check — runs inside one `try`/`catch`:\n * whatever throws, and for whatever reason, collapses to `'UnknownError'` exactly like any other\n * unrecognised name. `error.name` is still read exactly once, into `name` below, so a getter that\n * answers differently across separate calls has no second read left to answer differently on.\n */\nexport function loggableErrorName(error: unknown): string {\n try {\n if (!(error instanceof Error)) {\n return 'UnknownError';\n }\n\n const name = error.name;\n return LOGGABLE_ERROR_NAMES.has(name) ? name : 'UnknownError';\n } catch {\n return 'UnknownError';\n }\n}\n"],"mappings":";;;;;;;;;;;;AAWA,MAAM,uCAA4C,IAAI,IAAI;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;;AAgBD,SAAgB,kBAAkB,OAAwB;CACxD,IAAI;EACF,IAAI,EAAE,iBAAiB,QACrB,OAAO;EAGT,MAAM,OAAO,MAAM;EACnB,OAAO,qBAAqB,IAAI,IAAI,IAAI,OAAO;CACjD,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/utils/resolve-authored-copy.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resolve an authored copy value against its default. A store author can write "" or " " into any
|
|
4
|
+
* `type: 'string'` schema prop — both are valid JSON strings — and neither `??` nor a default
|
|
5
|
+
* parameter falls through on them, so the surface renders a blank label, legend or alert. Logical OR
|
|
6
|
+
* is not a fix either: " " is truthy.
|
|
7
|
+
*
|
|
8
|
+
* The authored value is returned VERBATIM when it has content — trimmed only to decide, never to
|
|
9
|
+
* render, matching `cart.tsx`'s `configuredLabel` and `region.tsx`'s `$content` idiom.
|
|
10
|
+
*/
|
|
11
|
+
function resolveAuthoredCopy(authored, fallback) {
|
|
12
|
+
if (authored == null) return fallback;
|
|
13
|
+
return authored.trim().length > 0 ? authored : fallback;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { resolveAuthoredCopy };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=resolve-authored-copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-authored-copy.js","names":[],"sources":["../../src/utils/resolve-authored-copy.ts"],"sourcesContent":["/**\n * Resolve an authored copy value against its default. A store author can write \"\" or \" \" into any\n * `type: 'string'` schema prop — both are valid JSON strings — and neither `??` nor a default\n * parameter falls through on them, so the surface renders a blank label, legend or alert. Logical OR\n * is not a fix either: \" \" is truthy.\n *\n * The authored value is returned VERBATIM when it has content — trimmed only to decide, never to\n * render, matching `cart.tsx`'s `configuredLabel` and `region.tsx`'s `$content` idiom.\n */\nexport function resolveAuthoredCopy(authored: string | undefined | null, fallback: string): string {\n if (authored == null) return fallback;\n return authored.trim().length > 0 ? authored : fallback;\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,oBAAoB,UAAqC,UAA0B;CACjG,IAAI,YAAY,MAAM,OAAO;CAC7B,OAAO,SAAS,KAAK,CAAC,CAAC,SAAS,IAAI,WAAW;AACjD"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/utils/substitute-template.ts
|
|
2
|
+
const TOKEN_PATTERN = /\{(\w+)\}/g;
|
|
3
|
+
/**
|
|
4
|
+
* Shared copy-template substitution (T97, T31 — specs/make-the-payment-components-authored-copy.md
|
|
5
|
+
* Decision 7). Replaces every seven-plus `String.prototype.replace('{token}', value)` call sites
|
|
6
|
+
* across `payment-summary.tsx`, `order-placed-payment-summary.tsx`, and `installments.tsx`, each of
|
|
7
|
+
* which substituted only the *first* occurrence of a token and, at two sites, passed the value as a
|
|
8
|
+
* replacement *string* — letting a server-sourced value containing `$&`/`` $` ``/`$'`/`$$` rewrite the
|
|
9
|
+
* surrounding template text.
|
|
10
|
+
*
|
|
11
|
+
* One `/\{(\w+)\}/g` pass with a replacer *function*: every occurrence of every token is substituted
|
|
12
|
+
* in a single scan, every value is inserted literally (a function's return value is never
|
|
13
|
+
* re-interpreted as a replacement pattern), and a token with no matching entry in `tokens` is left
|
|
14
|
+
* verbatim — so a template can be reworded freely without this helper silently dropping an
|
|
15
|
+
* unrecognized placeholder. Because it is one pass, a substituted value can never itself be re-scanned
|
|
16
|
+
* by a later token's own substitution.
|
|
17
|
+
*
|
|
18
|
+
* `Object.hasOwn`, not `tokenName in tokens` or a bare index lookup (fix pass, finding 6): every plain
|
|
19
|
+
* object inherits from `Object.prototype`, so `tokens['toString']` resolves to
|
|
20
|
+
* `Object.prototype.toString` — a function, therefore never `null`/`undefined` — rather than falling
|
|
21
|
+
* through to `match` on a `??` lookup. `{toString}`, `{constructor}`, and `{__proto__}` would each
|
|
22
|
+
* silently render an inherited value instead of being left verbatim as documented above.
|
|
23
|
+
*/
|
|
24
|
+
function substituteTemplate(template, tokens) {
|
|
25
|
+
return template.replace(TOKEN_PATTERN, (match, tokenName) => {
|
|
26
|
+
if (!Object.hasOwn(tokens, tokenName)) return match;
|
|
27
|
+
return tokens[tokenName] ?? match;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { substituteTemplate };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=substitute-template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"substitute-template.js","names":[],"sources":["../../src/utils/substitute-template.ts"],"sourcesContent":["const TOKEN_PATTERN = /\\{(\\w+)\\}/g;\n\n/**\n * Shared copy-template substitution (T97, T31 — specs/make-the-payment-components-authored-copy.md\n * Decision 7). Replaces every seven-plus `String.prototype.replace('{token}', value)` call sites\n * across `payment-summary.tsx`, `order-placed-payment-summary.tsx`, and `installments.tsx`, each of\n * which substituted only the *first* occurrence of a token and, at two sites, passed the value as a\n * replacement *string* — letting a server-sourced value containing `$&`/`` $` ``/`$'`/`$$` rewrite the\n * surrounding template text.\n *\n * One `/\\{(\\w+)\\}/g` pass with a replacer *function*: every occurrence of every token is substituted\n * in a single scan, every value is inserted literally (a function's return value is never\n * re-interpreted as a replacement pattern), and a token with no matching entry in `tokens` is left\n * verbatim — so a template can be reworded freely without this helper silently dropping an\n * unrecognized placeholder. Because it is one pass, a substituted value can never itself be re-scanned\n * by a later token's own substitution.\n *\n * `Object.hasOwn`, not `tokenName in tokens` or a bare index lookup (fix pass, finding 6): every plain\n * object inherits from `Object.prototype`, so `tokens['toString']` resolves to\n * `Object.prototype.toString` — a function, therefore never `null`/`undefined` — rather than falling\n * through to `match` on a `??` lookup. `{toString}`, `{constructor}`, and `{__proto__}` would each\n * silently render an inherited value instead of being left verbatim as documented above.\n */\nexport function substituteTemplate(template: string, tokens: Readonly<Record<string, string>>): string {\n return template.replace(TOKEN_PATTERN, (match, tokenName: string) => {\n if (!Object.hasOwn(tokens, tokenName)) return match;\n // `noUncheckedIndexedAccess` still types this lookup as `string | undefined`; the `Object.hasOwn`\n // check above is what actually guarantees a string is here, so this `??` can never fall through\n // for a real token — it only satisfies the index-signature type, never re-opens finding 6's bug.\n return tokens[tokenName] ?? match;\n });\n}\n"],"mappings":";AAAA,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;AAuBtB,SAAgB,mBAAmB,UAAkB,QAAkD;CACrG,OAAO,SAAS,QAAQ,gBAAgB,OAAO,cAAsB;EACnE,IAAI,CAAC,OAAO,OAAO,QAAQ,SAAS,GAAG,OAAO;EAI9C,OAAO,OAAO,cAAc;CAC9B,CAAC;AACH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vtex/faststore",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "FastStore AI Native component library built on Base UI with AI-editable styles",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://developers.vtex.com/docs/guides/faststore",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"@base-ui/react": "^1.3.0",
|
|
53
53
|
"@graphql-typed-document-node/core": "^3.2.0",
|
|
54
54
|
"graphql": "^16.13.1",
|
|
55
|
+
"react-hook-form": "^7.85.0",
|
|
55
56
|
"react-intl": "^10.1.22",
|
|
56
57
|
"rxjs": "^7.8.2",
|
|
57
58
|
"tailwindcss": "^4.2.2",
|
package/src/styles/tailwind.css
CHANGED
|
@@ -30,3 +30,37 @@
|
|
|
30
30
|
--max-width-md: var(--container-md);
|
|
31
31
|
--max-width-lg: var(--container-lg);
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
@layer base {
|
|
35
|
+
/* Default visible focus indicator for the payment surface (T38, REQ-11's WCAG scenario).
|
|
36
|
+
`@layer base` so any merchant utility class outranks it under Tailwind 4's layer order.
|
|
37
|
+
`currentColor` because the merchant's palette is unknown here — see Decision 6 on contrast.
|
|
38
|
+
Scoped by an explicit hook list, not by an ancestor: `PaymentSectionController` renders no DOM
|
|
39
|
+
element, so no ancestor selector reaches a controller-composed page. */
|
|
40
|
+
[data-fs-payment-method-list] :focus-visible,
|
|
41
|
+
[data-fs-installments] :focus-visible,
|
|
42
|
+
[data-fs-saved-cards] :focus-visible,
|
|
43
|
+
[data-fs-billing-address] :focus-visible,
|
|
44
|
+
[data-fs-card-entry] :focus-visible,
|
|
45
|
+
[data-fs-cardholder-document] :focus-visible,
|
|
46
|
+
[data-fs-save-card] :focus-visible,
|
|
47
|
+
[data-fs-place-order] :focus-visible,
|
|
48
|
+
[data-fs-installments-option]:focus-visible {
|
|
49
|
+
outline: 2px solid currentColor;
|
|
50
|
+
outline-offset: 2px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* `<fieldset>` carries `min-inline-size: min-content`, which Tailwind's preflight does not reset,
|
|
54
|
+
so it refuses to shrink inside a flex row. Live already via CardEntry's fieldset; mitigated here
|
|
55
|
+
for the three groups T38 adds. Scoped by the same explicit hook-list approach as the focus rule
|
|
56
|
+
above, not by `[data-fs-fieldset-root]`: the Fieldset particle emits that attribute
|
|
57
|
+
unconditionally, so it also reaches `ClientProfileForm`'s optional grouping fieldset — a
|
|
58
|
+
non-payment component whose rendering must not change (measured: 95px vs. 113px in a 200px flex
|
|
59
|
+
row with a competing sibling). */
|
|
60
|
+
[data-fs-payment-method-list-fieldset],
|
|
61
|
+
[data-fs-installments-fieldset],
|
|
62
|
+
[data-fs-saved-cards-fieldset],
|
|
63
|
+
[data-fs-card-entry-fieldset] {
|
|
64
|
+
min-inline-size: 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
# ClientProfileForm
|
|
2
|
-
|
|
3
|
-
**Category:** identity
|
|
4
|
-
**Purpose:** The batched customer-information form for the checkout identity step — the `InformationFormUSA` replacement. Pre-fills first/last name, phone, document, and both consent opt-ins from the shopper's saved profile and preferences; displays the email read-only. A single Save / Continue action validates the draft and dispatches at most one `clientProfileUpdate` and one `clientPreferencesUpdate`, each carrying only the fields the shopper edited, with pending/confirmation/error feedback.
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Visual cues (how to identify in an image)
|
|
9
|
-
|
|
10
|
-
- A single form block on a checkout identity step titled "Your information" (or similar), grouping: a read-only email display, first name, last name, a country-masked phone field, an optional tax-document field (e.g. CPF for Brazil), and two checkboxes ("email me with news and offers", "save my information for next time").
|
|
11
|
-
- One visible primary button at the bottom labelled "Save and continue" (or similar) — not a hidden submit.
|
|
12
|
-
- Pre-filled values for a returning/identified shopper; blank, still-editable fields for a guest or an incomplete profile.
|
|
13
|
-
- An inline error banner on failure, or a confirmation line on success, appearing near the button.
|
|
14
|
-
|
|
15
|
-
Use `ClientProfileForm` (not a hand-assembled combination of `Field`/`PhoneField`/`DocumentField`) whenever the image shows this whole batched-save customer-information block.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## When to use
|
|
20
|
-
|
|
21
|
-
- The customer-information section of the checkout identity step, for both returning/identified shoppers (pre-fill + edit) and guests (fill from empty).
|
|
22
|
-
- Anywhere name, phone, document, and consent opt-ins must be collected together and persisted in one explicit action.
|
|
23
|
-
|
|
24
|
-
## When NOT to use
|
|
25
|
-
|
|
26
|
-
- Editing the email — that is `EmailIdentificationControl`'s job; this form only displays it.
|
|
27
|
-
- A single standalone phone or opt-in field with its own save-on-blur behavior — use `ClientProfilePhoneControl` / `NewsletterOptInControl` / `SavePersonalDataOptInControl` instead. **Never place a standalone per-field identity Control and this form for the same field on the same view** — both would write the field, racing each other.
|
|
28
|
-
- Address fields — those belong to Delivery, not this form.
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## Composition
|
|
33
|
-
|
|
34
|
-
`ClientProfileForm` is a **compound**: `Root` owns the data + mechanics; the field/fieldset/feedback
|
|
35
|
-
parts are composed freely (any order, any subset). The schema author decides which fields render — a
|
|
36
|
-
merchant whose platform config drops a field (or whose account can't persist one, e.g. an unsupported
|
|
37
|
-
`savePersonalData`) simply does not compose that part.
|
|
38
|
-
|
|
39
|
-
```
|
|
40
|
-
SessionRoot (required ancestor; session tri-state + canEditData)
|
|
41
|
-
└── ClientProfileForm.Root (data-fs-client-profile-form; own fragment/data root; owns draft + Save)
|
|
42
|
-
├── ClientProfileForm.Fieldset section="profile" [legend]
|
|
43
|
-
│ ├── .EmailField (read-only)
|
|
44
|
-
│ ├── .FirstNameField / .LastNameField
|
|
45
|
-
│ ├── .PhoneField (uses country from Root)
|
|
46
|
-
│ └── .DocumentField (self-gates on documentType)
|
|
47
|
-
├── ClientProfileForm.Fieldset section="preferences" [legend]
|
|
48
|
-
│ ├── .NewsletterCheckbox
|
|
49
|
-
│ └── .SavePersonalDataCheckbox (omit to drop the field entirely — no CSS hack)
|
|
50
|
-
├── .Error / .Confirmation
|
|
51
|
-
└── .SubmitButton
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
`Root` is self-contained: it declares the colocated fragment over `clientProfile` + `clientPreferences`
|
|
55
|
-
and must not be nested inside `ClientProfileRoot` / `ClientPreferencesRoot` (those self-persist per
|
|
56
|
-
field on blur, double-writing against this form's batched Save). Every part `requiresContext:
|
|
57
|
-
'client-profile-form'`; a custom/derived field declares its own additional `requiresContext`.
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Behavior
|
|
62
|
-
|
|
63
|
-
- **Sections**: compose `ClientProfileForm.Fieldset` blocks with a `section` name → `data-fs-client-profile-form-section="<name>"`, and an optional `legend`. Not hard-coded — the author groups fields however the design needs.
|
|
64
|
-
- **Pre-fill**: every field seeds from `$data` on mount; missing profile fields render empty and stay editable.
|
|
65
|
-
- **Draft & dirty tracking**: edits are tracked locally per field; only touched fields are dispatched. Background `$data` refreshes re-seed untouched fields only — an in-progress edit is never clobbered.
|
|
66
|
-
- **Save**: validates **field format only** — phone mask and document check-digit, each blocking Save when the field is non-empty and malformed. It does not enforce requiredness: the parts are composed freely by the schema, so a required check here could fire for a field the page never mounted. Which fields a shopper must fill is a business rule, waiting on the composable entity forms RFC (#572) to decide where it belongs. The format checks carry the same composition hazard in a narrower form, and it is still open: they key off the Root's `country` and `documentType` rather than off which leaves were composed, so a store that sets `documentType` without composing `DocumentField` can block Save with no field to render the error in and nowhere to move focus. Closing it properly means the leaves registering themselves with the Root, which is that same RFC's territory. The phone case is wider than the document one and needs no merchant configuration at all — see Known gaps below. On Save it dispatches at most one `clientProfileUpdate` (dirty profile fields) and one `clientPreferencesUpdate` (dirty consent flags) in parallel; a Save with nothing dirty dispatches nothing and shows the confirmation, so the press still answers.
|
|
67
|
-
- **Document erase**: clearing a previously-saved document and pressing Save sends `document: ''` — an explicit erase, not an omission.
|
|
68
|
-
- **Feedback**: `data-fs-client-profile-form-pending` while a write is in flight (fieldset disabled, Save not re-triggerable); an inline `role="alert"` region on `userErrors`; an `<output>` confirmation on success, cleared on the next edit.
|
|
69
|
-
- **Read-only**: `session.canEditData === false` renders `data-fs-client-profile-form-readonly`, disables every field, and omits the Save button entirely.
|
|
70
|
-
- **The form announces nothing**: a completed Save shows the confirmation and stops there. It publishes no signal, so nothing elsewhere collapses, advances or reacts. Whether finishing this form should move a section or a wizard is a sequencing question, and the mechanism for it is being designed in the flow-of-steps RFC (#611) rather than assumed here.
|
|
71
|
-
- **The form never learns who listens.** It reports nothing at all, so `IdentityEditController` cannot react to it: that section moves only on its own buttons. Compose the swap around this form: an `IdentityEditController` holding an `IdentitySummaryView` with the summary, an `IdentityEditForm` with `ClientProfileForm.Root`, an `IdentityEditButton` in the heading, and an `IdentityEditCancelButton` as cancel next to the form. The same form therefore works unchanged inside a collapsing section or on a page of its own.
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
## Props
|
|
76
|
-
|
|
77
|
-
**`ClientProfileForm.Root`** — `country` (`'US' | 'BR'`, default `'US'`; provided to the phone leaf via
|
|
78
|
-
context), `documentType` (string, e.g. `'CPF'`; the document leaf renders only when supported).
|
|
79
|
-
Declares `actions: { identity: ['clientProfileUpdate', 'clientPreferencesUpdate'] }`,
|
|
80
|
-
`requiresContext: 'session'`, `providesContext: 'client-profile-form'`.
|
|
81
|
-
|
|
82
|
-
**`ClientProfileForm.Fieldset`** — `legend` (optional visible title), `section` (name → `data-fs-*-section`).
|
|
83
|
-
|
|
84
|
-
**Field parts** (`.EmailField`, `.FirstNameField`, `.LastNameField`, `.PhoneField`, `.DocumentField`,
|
|
85
|
-
`.NewsletterCheckbox`, `.SavePersonalDataCheckbox`) — each takes an optional `label` (sensible default).
|
|
86
|
-
**`.Confirmation`** — `message` (default `'Your information has been saved.'`). **`.SubmitButton`** —
|
|
87
|
-
`label` (default `'Save and continue'`) or composed `children`.
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## Canonical schema example
|
|
92
|
-
|
|
93
|
-
```json
|
|
94
|
-
{
|
|
95
|
-
"session": {
|
|
96
|
-
"type": "SessionRoot",
|
|
97
|
-
"children": ["email-identification", "profile-form"]
|
|
98
|
-
},
|
|
99
|
-
"email-identification": {
|
|
100
|
-
"type": "EmailIdentificationControl",
|
|
101
|
-
"children": []
|
|
102
|
-
},
|
|
103
|
-
"profile-form": {
|
|
104
|
-
"type": "ClientProfileFormRoot",
|
|
105
|
-
"props": { "country": "BR", "documentType": "CPF" },
|
|
106
|
-
"children": ["profile-fieldset", "preferences-fieldset", "form-error", "form-confirmation", "form-submit"]
|
|
107
|
-
},
|
|
108
|
-
"profile-fieldset": {
|
|
109
|
-
"type": "ClientProfileFormFieldset",
|
|
110
|
-
"props": { "section": "profile", "legend": "Your information" },
|
|
111
|
-
"children": ["email", "first-name", "last-name", "phone", "document"]
|
|
112
|
-
},
|
|
113
|
-
"email": { "type": "ClientProfileFormEmailField", "children": [] },
|
|
114
|
-
"first-name": { "type": "ClientProfileFormFirstNameField", "children": [] },
|
|
115
|
-
"last-name": { "type": "ClientProfileFormLastNameField", "children": [] },
|
|
116
|
-
"phone": { "type": "ClientProfileFormPhoneField", "children": [] },
|
|
117
|
-
"document": { "type": "ClientProfileFormDocumentField", "children": [] },
|
|
118
|
-
"preferences-fieldset": {
|
|
119
|
-
"type": "ClientProfileFormFieldset",
|
|
120
|
-
"props": { "section": "preferences" },
|
|
121
|
-
"children": ["newsletter"]
|
|
122
|
-
},
|
|
123
|
-
"newsletter": { "type": "ClientProfileFormNewsletterCheckbox", "children": [] },
|
|
124
|
-
"form-error": { "type": "ClientProfileFormError", "children": [] },
|
|
125
|
-
"form-confirmation": { "type": "ClientProfileFormConfirmation", "children": [] },
|
|
126
|
-
"form-submit": { "type": "ClientProfileFormSubmitButton", "children": [] }
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Both fieldsets are children of the Root, not siblings of it: every part declares
|
|
131
|
-
`requiresContext: 'client-profile-form'`, so a fieldset placed outside is rejected by the
|
|
132
|
-
context-constraint walk at build time.
|
|
133
|
-
|
|
134
|
-
(Omitting `ClientProfileFormSavePersonalDataCheckbox` above drops that field entirely — the intended
|
|
135
|
-
way to exclude a field the account can't persist, with no CSS hack and no shared-component change.)
|
|
136
|
-
|
|
137
|
-
---
|
|
138
|
-
|
|
139
|
-
## Common mistakes
|
|
140
|
-
|
|
141
|
-
- **Rendering `ClientProfileForm` outside `SessionRoot`.** `useSession()` throws; every identity page must host a `SessionRoot`.
|
|
142
|
-
- **Nesting it inside `ClientProfileRoot` / `ClientPreferencesRoot`, or placing a standalone Control for the same field alongside it.** The form is its own data root and its own write surface — double-nesting or double-placing creates a double-write race.
|
|
143
|
-
- **Expecting the email to be editable.** It is always read-only here; email changes go through `EmailIdentificationControl`.
|
|
144
|
-
- **Expecting per-keystroke or per-blur saves.** All fields are form-bound; only pressing Save dispatches a write.
|
|
145
|
-
- **Expecting the form to enforce required fields.** It validates format only. A blank field saves, because the form cannot know which of its freely-composed parts the page actually mounted.
|
|
146
|
-
- **Rendering a field part outside `ClientProfileForm.Root`.** Every part `requiresContext: 'client-profile-form'` and throws otherwise.
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
## Styling
|
|
151
|
-
|
|
152
|
-
| Component | What you style | Plausible classes |
|
|
153
|
-
|---|---|---|
|
|
154
|
-
| `ClientProfileForm` | The `data-fs-client-profile-form` root; carries `-pending`/`-readonly`/`-saved` presence attributes | `flex flex-col gap-10` |
|
|
155
|
-
| Section | `data-fs-client-profile-form-section="profile\|preferences"` — the two entity-anchored fieldsets | `flex flex-col gap-4` |
|
|
156
|
-
| Field wrapper | `data-fs-client-profile-form-field="email\|first-name\|last-name\|phone\|document\|newsletter\|save-personal-data"` | `flex flex-col gap-4` |
|
|
157
|
-
| Error / confirmation | `[data-fs-client-profile-form-error]` / `[data-fs-client-profile-form-confirmation]` | `text-sm text-red-600` / `text-sm text-green-600` |
|
|
158
|
-
|
|
159
|
-
Field-level visuals (borders, focus rings, error color) are owned by the composed particles' own swatches (`field`, `phone-field`, `document-field`, `checkbox`, `button`) and are not redefined here.
|
|
160
|
-
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
## Known gaps
|
|
164
|
-
|
|
165
|
-
Found by review and deliberately left for the dedicated revision of these components, rather than patched
|
|
166
|
-
here. The first is the widest.
|
|
167
|
-
|
|
168
|
-
- **Format validation runs against fields the page may never have composed, and the phone case needs no
|
|
169
|
-
merchant configuration at all.** `country` defaults to `'US'`, so the phone check always runs. A store
|
|
170
|
-
composing `Root` + name fields + `SubmitButton`, serving a shopper whose stored phone is a legacy
|
|
171
|
-
malformed value, gets a Save that sets `errors.phone`, finds no phone input to focus, renders no message
|
|
172
|
-
because `PhoneField` is not mounted, and returns — dead on every press, button still enabled. The
|
|
173
|
-
`documentType` variant of the same hole at least requires the merchant to have set a Root prop. Closing
|
|
174
|
-
both properly means the leaves registering themselves with the Root; see the composable entity forms RFC
|
|
175
|
-
(#572).
|
|
176
|
-
- **Two other early returns leave Save silent.** A missing `$actions.identity`, or a dirty field with no
|
|
177
|
-
matching write function, returns without touching `pending`, `serverError` or `saved` — the same dead
|
|
178
|
-
press the pristine branch above was changed to avoid.
|
|
179
|
-
- **`VALIDATION_ORDER` is a hard-coded `['phone', 'document']`,** but the leaves are now composed in any
|
|
180
|
-
order. With both fields malformed, focus jumps to the phone input even when the document error is the one
|
|
181
|
-
the shopper can see first. The order should come from DOM position now that the fixed template is gone.
|
|
182
|
-
- **The re-seed of untouched fields mutates a ref during render** and queues its `setDraft` in the same
|
|
183
|
-
pass. A discarded render keeps the ref but loses the update, so pristine fields can hold stale values
|
|
184
|
-
after `$data` arrives.
|
|
185
|
-
|
|
186
|
-
## Related
|
|
187
|
-
|
|
188
|
-
- [[email-identification]] — the sibling identification control this form's email display defers to.
|
|
189
|
-
- [[client-profile]] — the standalone, per-field-blur phone binding; do not compose alongside this form for the same field.
|
|
190
|
-
- [[client-preferences]] — the standalone, per-field-blur consent Controls; do not compose alongside this form for the same field.
|
|
191
|
-
- [[session]] — provides the identification tri-state and the required `SessionRoot` ancestor.
|
|
192
|
-
- [[phone-field]], [[document-field]], [[field]], [[fieldset]], [[checkbox]] — the particles this matter composes directly.
|