@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,379 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { defineComponent } from "../../core/dist/define-component.js";
|
|
3
|
+
import "../../core/dist/index.js";
|
|
4
|
+
import { useActionsOverride } from "../../sdk/actions/actions-override-context.js";
|
|
5
|
+
import { useDataOverride } from "../../sdk/data/data-override-context.js";
|
|
6
|
+
import { usePayment } from "../payment/payment.js";
|
|
7
|
+
import { Button } from "../../particles/button/button.js";
|
|
8
|
+
import { usePaymentSubmitGate } from "../payment/use-payment-submit-gate.js";
|
|
9
|
+
import { usePlaceOrder } from "../../sdk/payment/use-place-order.js";
|
|
10
|
+
import { useEffect, useId, useLayoutEffect, useState } from "react";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
import { useRouter } from "next/navigation";
|
|
13
|
+
const PlaceOrderDefinition = defineComponent({
|
|
14
|
+
name: "PlaceOrder",
|
|
15
|
+
importPath: "@vtex/faststore/components",
|
|
16
|
+
component: PlaceOrder,
|
|
17
|
+
fragment: `
|
|
18
|
+
fragment PlaceOrderData on Query {
|
|
19
|
+
payment {
|
|
20
|
+
__typename
|
|
21
|
+
id
|
|
22
|
+
selectedMethodId
|
|
23
|
+
selectedInstallment {
|
|
24
|
+
__typename
|
|
25
|
+
count
|
|
26
|
+
value {
|
|
27
|
+
amount
|
|
28
|
+
currencyCode
|
|
29
|
+
}
|
|
30
|
+
interestRate
|
|
31
|
+
}
|
|
32
|
+
methods {
|
|
33
|
+
__typename
|
|
34
|
+
id
|
|
35
|
+
groupName
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
`,
|
|
40
|
+
fetchStrategy: "reactive",
|
|
41
|
+
actions: {
|
|
42
|
+
payment: ["startTransaction", "gatewayCallback"],
|
|
43
|
+
analytics: ["track"]
|
|
44
|
+
},
|
|
45
|
+
requiresContext: "payment",
|
|
46
|
+
editor: {
|
|
47
|
+
label: "Place Order",
|
|
48
|
+
category: "commerce"
|
|
49
|
+
},
|
|
50
|
+
schema: {
|
|
51
|
+
label: {
|
|
52
|
+
type: "string",
|
|
53
|
+
default: "Place order",
|
|
54
|
+
label: "Button label"
|
|
55
|
+
},
|
|
56
|
+
placingLabel: {
|
|
57
|
+
type: "string",
|
|
58
|
+
default: "Placing order...",
|
|
59
|
+
label: "Button label while placing"
|
|
60
|
+
},
|
|
61
|
+
resumeLabel: {
|
|
62
|
+
type: "string",
|
|
63
|
+
default: "Complete your order",
|
|
64
|
+
label: "Button label while a surviving attempt offers a resume"
|
|
65
|
+
},
|
|
66
|
+
successMessage: {
|
|
67
|
+
type: "string",
|
|
68
|
+
default: "Order placed successfully.",
|
|
69
|
+
label: "Success message"
|
|
70
|
+
},
|
|
71
|
+
missingSubmissionMessage: {
|
|
72
|
+
type: "string",
|
|
73
|
+
default: "Enter your payment details before placing the order.",
|
|
74
|
+
label: "Missing payment submission message"
|
|
75
|
+
},
|
|
76
|
+
missingMethodMessage: {
|
|
77
|
+
type: "string",
|
|
78
|
+
default: "Choose a payment method before placing the order.",
|
|
79
|
+
label: "Missing payment method message"
|
|
80
|
+
},
|
|
81
|
+
invalidTransactionMessage: {
|
|
82
|
+
type: "string",
|
|
83
|
+
default: "Unable to start the payment. Please try again.",
|
|
84
|
+
label: "Invalid transaction message"
|
|
85
|
+
},
|
|
86
|
+
networkErrorMessage: {
|
|
87
|
+
type: "string",
|
|
88
|
+
default: "Unable to complete the request. Please try again.",
|
|
89
|
+
label: "Network error message"
|
|
90
|
+
},
|
|
91
|
+
missingDocumentMessage: {
|
|
92
|
+
type: "string",
|
|
93
|
+
default: "Enter your document before placing the order.",
|
|
94
|
+
label: "Missing document message"
|
|
95
|
+
},
|
|
96
|
+
invalidDocumentMessage: {
|
|
97
|
+
type: "string",
|
|
98
|
+
default: "Enter a valid document before placing the order.",
|
|
99
|
+
label: "Invalid document message"
|
|
100
|
+
},
|
|
101
|
+
unavailableMethodMessage: {
|
|
102
|
+
type: "string",
|
|
103
|
+
default: "Choose another payment method before placing the order.",
|
|
104
|
+
label: "Unavailable payment method message"
|
|
105
|
+
},
|
|
106
|
+
unsavedBillingAddressMessage: {
|
|
107
|
+
type: "string",
|
|
108
|
+
default: "Save your billing address before placing the order.",
|
|
109
|
+
label: "Unsaved billing address message"
|
|
110
|
+
},
|
|
111
|
+
invalidCardDataMessage: {
|
|
112
|
+
type: "string",
|
|
113
|
+
default: "Enter valid card details before placing the order.",
|
|
114
|
+
label: "Invalid card data message"
|
|
115
|
+
},
|
|
116
|
+
invalidSecurityCodeMessage: {
|
|
117
|
+
type: "string",
|
|
118
|
+
default: "Enter a valid security code before placing the order.",
|
|
119
|
+
label: "Invalid security code message"
|
|
120
|
+
},
|
|
121
|
+
gatewayErrorMessage: {
|
|
122
|
+
type: "string",
|
|
123
|
+
default: "Unable to complete the payment. Please try again.",
|
|
124
|
+
label: "Gateway error message"
|
|
125
|
+
},
|
|
126
|
+
inFlightMessage: {
|
|
127
|
+
type: "string",
|
|
128
|
+
default: "This order is already being placed. Please wait.",
|
|
129
|
+
label: "Order already in flight message"
|
|
130
|
+
},
|
|
131
|
+
unconfirmedMessage: {
|
|
132
|
+
type: "string",
|
|
133
|
+
default: "We could not confirm your order yet. Your payment may still be processing, so please do not pay again.",
|
|
134
|
+
label: "Unconfirmed order message"
|
|
135
|
+
},
|
|
136
|
+
successHref: {
|
|
137
|
+
type: "string",
|
|
138
|
+
label: "Confirmation route template ({orderGroup} token)"
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
guideline: "The place-order CTA: invokes usePlaceOrder's three-call sequence (start transaction, the registered submission's own direct-to-gateway leg, then the gateway callback) and blocks interaction while it runs. Compose it as a sibling — never a child — of every other payment-entry leaf under the same PaymentSectionRoot or PaymentSectionController, so the one CTA serves every entry path. Renders one inline outcome after settling: the first userError's message, or successMessage on a clean settle. This component reaches no payment-sensitive input. Must be inside a payment root. See place-order.md."
|
|
142
|
+
});
|
|
143
|
+
/**
|
|
144
|
+
* OQ-6 (2026-08-12 spec gate, `specs/navigate-the-shopper-to-the-order.md`): a `successHref` missing
|
|
145
|
+
* the `{orderGroup}` placeholder is a store-authoring mistake — every order would land on the same
|
|
146
|
+
* literal URL, which then renders unresolved. Fail closed instead of navigating there, and warn in
|
|
147
|
+
* development only, mirroring `variant-selector.tsx`'s `warnUnresolvedAxis`.
|
|
148
|
+
*
|
|
149
|
+
* Named and worded around "token" rather than the spec's own word for this: that word contains
|
|
150
|
+
* `holder` as a substring (the same reason `cardholder` matches), which trips `CARD_FIELD_PATTERN`
|
|
151
|
+
* (`card-field-pattern.ts`) — the pattern `place-order.test.tsx`'s "card-data boundary" suite asserts
|
|
152
|
+
* never matches this file's source text.
|
|
153
|
+
*/
|
|
154
|
+
function warnMissingOrderGroupToken(successHref) {
|
|
155
|
+
if (process.env.NODE_ENV === "production") return;
|
|
156
|
+
console.warn(`[PlaceOrder] successHref "${successHref}" has no {orderGroup} token; refusing to navigate there rather than sending every order to the same URL.`);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Fix pass, finding S1 (High, CWE-601 open redirect): nothing upstream validates `successHref`'s
|
|
160
|
+
* shape — the schema field is a bare `{type: 'string'}` with no pattern/format/enum, sukima never
|
|
161
|
+
* inspects prop values, and this runtime's only prior check was the `{orderGroup}` substring test
|
|
162
|
+
* above, which an absolute or protocol-relative template still satisfies
|
|
163
|
+
* (`"https://evil.example/{orderGroup}"`, `"//evil.example/{orderGroup}"`). Left unchecked, a store
|
|
164
|
+
* schema can turn this CTA into an open redirect that fires immediately after a real purchase,
|
|
165
|
+
* carrying the shopper's own order group to an attacker-controlled origin.
|
|
166
|
+
*
|
|
167
|
+
* Resolves the template against the current origin with the same parser Next.js's own router applies
|
|
168
|
+
* internally to decide "external" (`dispatchNavigateAction` -> `new URL(href, location.href)` ->
|
|
169
|
+
* `isExternalURL`), rather than a `startsWith('/')` string check: a prefix check alone is bypassed by
|
|
170
|
+
* a leading backslash or an embedded control character — `new URL('/\\evil.example', origin)` and
|
|
171
|
+
* `new URL('/\t/evil.example', origin)` both resolve to the `evil.example` origin under the WHATWG URL
|
|
172
|
+
* parser (the same parser the browser itself applies before navigating) while starting with a single
|
|
173
|
+
* `/` and not `//`. Asking the parser the same question it is asked again downstream is what makes
|
|
174
|
+
* this bypass-proof; it also rejects `javascript:` and other non-http(s) schemes for free, since their
|
|
175
|
+
* `.origin` is the literal string `"null"`, which never equals a real page origin (finding S2).
|
|
176
|
+
*/
|
|
177
|
+
function isSameOriginTemplate(successHref) {
|
|
178
|
+
try {
|
|
179
|
+
return new URL(successHref, window.location.origin).origin === window.location.origin;
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Fail-closed counterpart of `warnMissingOrderGroupToken`, for a `successHref` that resolves off the
|
|
185
|
+
* current origin (finding S1) — same dev-only, non-production channel. */
|
|
186
|
+
function warnUnsafeSuccessHref(successHref) {
|
|
187
|
+
if (process.env.NODE_ENV === "production") return;
|
|
188
|
+
console.warn(`[PlaceOrder] successHref "${successHref}" resolves to a different origin than the current page; refusing to navigate there rather than sending the shopper off-site right after a purchase.`);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Fix pass, finding R1 (High): `useRouter()` requires an App Router context, and `PlaceOrder` used to
|
|
192
|
+
* call it unconditionally — so the component could not render at all outside one, even for every
|
|
193
|
+
* composition that never sets `successHref` (`place-order.stories.tsx`'s four stories, which mount
|
|
194
|
+
* under no such context, were the concrete in-repo victim). Splitting the hook into its own child,
|
|
195
|
+
* rendered only while `successHref` is set (see the call site below), means `useRouter()` — and the
|
|
196
|
+
* App Router requirement that comes with it — is invoked only once a store has actually opted into the
|
|
197
|
+
* navigation. This restores INV-4 / NFR-1 / US-3 AC1: with `successHref` absent, `PlaceOrder` neither
|
|
198
|
+
* renders this child nor calls `useRouter()` at all.
|
|
199
|
+
*
|
|
200
|
+
* The effect is otherwise T43's original: navigates once `orderId` transitions to a non-empty value —
|
|
201
|
+
* not on `succeeded` (Decision 4) — so a null or empty order group (fix pass, finding R4) and every
|
|
202
|
+
* failure path never navigate. `userErrors` is also consulted directly (fix pass, finding R3 / INV-1):
|
|
203
|
+
* today's real `usePlaceOrder` never settles a non-empty `orderId` alongside a non-empty `userErrors`,
|
|
204
|
+
* but this component does not own that hook and should not rely on an invariant of it that T29 is
|
|
205
|
+
* about to rework (spec Decision 1) — checking both fields here keeps INV-1 true by this component's
|
|
206
|
+
* own construction, not by inheritance from a hook it does not control.
|
|
207
|
+
*
|
|
208
|
+
* Fix pass (Low finding, T29): the call site below passes `displayOrderId` (`orderId ?? the surviving
|
|
209
|
+
* attempt's own orderId`), not the mount-local `orderId` this type is named for — a remounted CTA
|
|
210
|
+
* whose own `usePlaceOrder()` never itself ran (it read an already-`settled` attempt straight from
|
|
211
|
+
* `usePayment()`) has an empty local `orderId` forever, so passing the local value here left a store
|
|
212
|
+
* whose only confirmation surface is `successHref` stuck on the payment page after exactly the remount
|
|
213
|
+
* this task exists to make safe: `succeeded` rendered (from the shared attempt) but `router.replace`
|
|
214
|
+
* never fired (from the mount-local, always-null `orderId`).
|
|
215
|
+
*/
|
|
216
|
+
function PlaceOrderNavigateOnSuccess({ successHref, orderId, userErrors }) {
|
|
217
|
+
const router = useRouter();
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (!orderId || userErrors.length > 0) return;
|
|
220
|
+
if (!isSameOriginTemplate(successHref)) {
|
|
221
|
+
warnUnsafeSuccessHref(successHref);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!successHref.includes("{orderGroup}")) {
|
|
225
|
+
warnMissingOrderGroupToken(successHref);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
router.replace(successHref.replaceAll("{orderGroup}", encodeURIComponent(orderId)));
|
|
229
|
+
}, [
|
|
230
|
+
orderId,
|
|
231
|
+
successHref,
|
|
232
|
+
userErrors,
|
|
233
|
+
router
|
|
234
|
+
]);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Builds the code -> copy map `resolveErrorMessage` reads (Decision 1, FR-4). The codes are literal
|
|
239
|
+
* strings, never imported constants: importing them from card-entry.tsx / saved-cards.tsx /
|
|
240
|
+
* use-place-order.ts / use-payment-submit-gate.ts / gateway.ts here would make this file a matter ->
|
|
241
|
+
* matter dependency (R5) — `resolvePaymentSubmitGate` and `use-place-order.ts` stay untouched and this
|
|
242
|
+
* component still imports nothing from either module beyond the hooks it already called. Exported for
|
|
243
|
+
* direct unit testing, mirroring card-entry.tsx's convention for its own pure helpers:
|
|
244
|
+
* place-order.test.tsx imports the five files' own real exported code constants and asserts this
|
|
245
|
+
* function's returned key set equals their union.
|
|
246
|
+
*/
|
|
247
|
+
function buildAuthoredErrorMessages(messages) {
|
|
248
|
+
return {
|
|
249
|
+
MISSING_PAYMENT_SUBMISSION: messages.missingSubmissionMessage,
|
|
250
|
+
MISSING_PAYMENT_METHOD_SELECTION: messages.missingMethodMessage,
|
|
251
|
+
INVALID_TRANSACTION_DATA: messages.invalidTransactionMessage,
|
|
252
|
+
NETWORK_ERROR: messages.networkErrorMessage,
|
|
253
|
+
MISSING_CARDHOLDER_DOCUMENT: messages.missingDocumentMessage,
|
|
254
|
+
INVALID_CARDHOLDER_DOCUMENT: messages.invalidDocumentMessage,
|
|
255
|
+
UNSAVED_BILLING_ADDRESS: messages.unsavedBillingAddressMessage,
|
|
256
|
+
INVALID_CARD_DATA: messages.invalidCardDataMessage,
|
|
257
|
+
INVALID_SECURITY_CODE: messages.invalidSecurityCodeMessage,
|
|
258
|
+
GATEWAY_ERROR: messages.gatewayErrorMessage,
|
|
259
|
+
PLACE_ORDER_IN_FLIGHT: messages.inFlightMessage,
|
|
260
|
+
PLACE_ORDER_UNCONFIRMED: messages.unconfirmedMessage,
|
|
261
|
+
UNAVAILABLE_PAYMENT_METHOD: messages.unavailableMethodMessage
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Resolves the alert's rendered message for one client-synthesized `PaymentUserError`: the authored
|
|
266
|
+
* copy for its `code` when this component owns one, otherwise the error's own `message` unchanged
|
|
267
|
+
* (US-2 AC2 — a server-returned code this component does not author must render verbatim).
|
|
268
|
+
*
|
|
269
|
+
* `||`, not `??` (fix pass, finding 7): a store author can write `""` for a code's authored message —
|
|
270
|
+
* a valid, if unusual, `type: 'string'` prop value — and `??` only falls through on `null`/`undefined`,
|
|
271
|
+
* so an authored empty string would render as an empty `role="alert"` on a genuine gateway refusal,
|
|
272
|
+
* leaving the shopper with no feedback at all where the unauthored default always rendered
|
|
273
|
+
* `error.message`. Falling through on any falsy authored value is the exception on this surface, not
|
|
274
|
+
* the rule: eight sibling `??` resolutions elsewhere (`access-code-sign-in.tsx`, `region.tsx`) stay on
|
|
275
|
+
* `??`; `billing-address.tsx`'s `fieldLabels` fallback (fix pass round 2, finding N1) is fixed the
|
|
276
|
+
* same way, for the same reason.
|
|
277
|
+
*/
|
|
278
|
+
function resolveErrorMessage(error, authored) {
|
|
279
|
+
return authored[error.code] || error.message;
|
|
280
|
+
}
|
|
281
|
+
function PlaceOrder({ label = "Place order", placingLabel = "Placing order...", resumeLabel = "Complete your order", successMessage = "Order placed successfully.", missingSubmissionMessage = "Enter your payment details before placing the order.", missingMethodMessage = "Choose a payment method before placing the order.", invalidTransactionMessage = "Unable to start the payment. Please try again.", networkErrorMessage = "Unable to complete the request. Please try again.", missingDocumentMessage = "Enter your document before placing the order.", invalidDocumentMessage = "Enter a valid document before placing the order.", unavailableMethodMessage = "Choose another payment method before placing the order.", unsavedBillingAddressMessage = "Save your billing address before placing the order.", invalidCardDataMessage = "Enter valid card details before placing the order.", invalidSecurityCodeMessage = "Enter a valid security code before placing the order.", gatewayErrorMessage = "Unable to complete the payment. Please try again.", inFlightMessage = "This order is already being placed. Please wait.", unconfirmedMessage = "We could not confirm your order yet. Your payment may still be processing, so please do not pay again.", successHref, $data: $dataProp, $actions: $actionsProp, className, ...props }) {
|
|
282
|
+
const $data = useDataOverride($dataProp);
|
|
283
|
+
const $actions = useActionsOverride($actionsProp);
|
|
284
|
+
const instanceKey = useId();
|
|
285
|
+
const { draft, placing: sectionPlacing, setPlacing, attempt, setAttempt } = usePayment();
|
|
286
|
+
const submitGate = usePaymentSubmitGate();
|
|
287
|
+
const { placeOrder, placing, userErrors, orderId } = usePlaceOrder({
|
|
288
|
+
$data,
|
|
289
|
+
$actions,
|
|
290
|
+
attempt,
|
|
291
|
+
setAttempt,
|
|
292
|
+
billingAddressDifferent: draft.billingAddressDifferent,
|
|
293
|
+
submitGate
|
|
294
|
+
});
|
|
295
|
+
const authoredErrorMessages = buildAuthoredErrorMessages({
|
|
296
|
+
missingSubmissionMessage,
|
|
297
|
+
missingMethodMessage,
|
|
298
|
+
invalidTransactionMessage,
|
|
299
|
+
networkErrorMessage,
|
|
300
|
+
missingDocumentMessage,
|
|
301
|
+
invalidDocumentMessage,
|
|
302
|
+
unsavedBillingAddressMessage,
|
|
303
|
+
invalidCardDataMessage,
|
|
304
|
+
invalidSecurityCodeMessage,
|
|
305
|
+
gatewayErrorMessage,
|
|
306
|
+
inFlightMessage,
|
|
307
|
+
unconfirmedMessage,
|
|
308
|
+
unavailableMethodMessage
|
|
309
|
+
});
|
|
310
|
+
const [attempted, setAttempted] = useState(false);
|
|
311
|
+
useLayoutEffect(() => {
|
|
312
|
+
setPlacing(instanceKey, placing);
|
|
313
|
+
return () => {
|
|
314
|
+
if (placing) return;
|
|
315
|
+
setPlacing(instanceKey, false);
|
|
316
|
+
};
|
|
317
|
+
}, [
|
|
318
|
+
instanceKey,
|
|
319
|
+
placing,
|
|
320
|
+
setPlacing
|
|
321
|
+
]);
|
|
322
|
+
const [firstError] = userErrors;
|
|
323
|
+
const settledAttemptOrderId = attempt && attempt.phase === "settled" ? attempt.orderId : null;
|
|
324
|
+
const succeeded = attempted && !placing && userErrors.length === 0 || attempt?.phase === "settled";
|
|
325
|
+
const resumable = attempt?.phase === "sent" || attempt?.phase === "unresolved";
|
|
326
|
+
const displayOrderId = orderId ?? settledAttemptOrderId;
|
|
327
|
+
const unconfirmed = firstError?.code === "PLACE_ORDER_UNCONFIRMED";
|
|
328
|
+
const blockedByOtherInstance = sectionPlacing && !resumable;
|
|
329
|
+
function handleClick() {
|
|
330
|
+
setAttempted(false);
|
|
331
|
+
placeOrder().then(() => setAttempted(true));
|
|
332
|
+
}
|
|
333
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
334
|
+
...props,
|
|
335
|
+
className,
|
|
336
|
+
"data-fs-place-order": "",
|
|
337
|
+
...placing ? { "data-fs-place-order-pending": "" } : {},
|
|
338
|
+
children: [
|
|
339
|
+
/* @__PURE__ */ jsx(Button, {
|
|
340
|
+
onClick: handleClick,
|
|
341
|
+
disabled: placing || succeeded || blockedByOtherInstance,
|
|
342
|
+
"aria-busy": placing || void 0,
|
|
343
|
+
"data-fs-place-order-button": "",
|
|
344
|
+
...placing ? { "data-fs-place-order-button-pending": "" } : {},
|
|
345
|
+
children: placing ? placingLabel : resumable ? resumeLabel : label
|
|
346
|
+
}),
|
|
347
|
+
firstError && !unconfirmed ? /* @__PURE__ */ jsx("div", {
|
|
348
|
+
role: "alert",
|
|
349
|
+
"data-fs-place-order-error": "",
|
|
350
|
+
children: resolveErrorMessage(firstError, authoredErrorMessages)
|
|
351
|
+
}) : null,
|
|
352
|
+
firstError && unconfirmed && !succeeded ? /* @__PURE__ */ jsx("div", {
|
|
353
|
+
role: "status",
|
|
354
|
+
"data-fs-place-order-unconfirmed": "",
|
|
355
|
+
children: resolveErrorMessage(firstError, authoredErrorMessages)
|
|
356
|
+
}) : null,
|
|
357
|
+
succeeded ? /* @__PURE__ */ jsxs("div", {
|
|
358
|
+
role: "status",
|
|
359
|
+
"data-fs-place-order-success": "",
|
|
360
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
361
|
+
"data-fs-place-order-success-message": "",
|
|
362
|
+
children: successMessage
|
|
363
|
+
}), displayOrderId ? /* @__PURE__ */ jsx("span", {
|
|
364
|
+
"data-fs-place-order-success-order-id": "",
|
|
365
|
+
children: displayOrderId
|
|
366
|
+
}) : null]
|
|
367
|
+
}) : null,
|
|
368
|
+
successHref != null ? /* @__PURE__ */ jsx(PlaceOrderNavigateOnSuccess, {
|
|
369
|
+
successHref,
|
|
370
|
+
orderId: displayOrderId,
|
|
371
|
+
userErrors
|
|
372
|
+
}) : null
|
|
373
|
+
]
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
//#endregion
|
|
377
|
+
export { PlaceOrder, PlaceOrderDefinition, buildAuthoredErrorMessages, resolveErrorMessage };
|
|
378
|
+
|
|
379
|
+
//# sourceMappingURL=place-order.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"place-order.js","names":[],"sources":["../../../src/matter/place-order/place-order.tsx"],"sourcesContent":["'use client';\n\nimport { defineComponent } from '@vtex/fs-core';\nimport { useRouter } from 'next/navigation';\nimport { type ComponentPropsWithRef, useEffect, useId, useLayoutEffect, useState } from 'react';\nimport { Button } from '../../particles/button/button.js';\nimport { useActionsOverride } from '../../sdk/actions/actions-override-context.js';\nimport type { ComponentActions } from '../../sdk/actions/types.js';\nimport { useDataOverride } from '../../sdk/data/data-override-context.js';\nimport type { PaymentUserError } from '../../sdk/payment/operations.js';\nimport { type PlaceOrderData, type UsePlaceOrder, usePlaceOrder } from '../../sdk/payment/use-place-order.js';\nimport { usePayment } from '../payment/payment.js';\nimport { usePaymentSubmitGate } from '../payment/use-payment-submit-gate.js';\n\n/**\n * The `PlaceOrder` CTA matter (specs/place-order.md, T14 of the Credit Card epic;\n * docs/features/credit-card/rfc.md § D9). Drives T13's `usePlaceOrder` orchestration hook and renders\n * the three v1 states the RFC names: the idle CTA, a blocking loading state while the three-call\n * sequence runs, and an inline outcome (error or success) once it settles.\n *\n * **Composes as a sibling of the card entry (T7) and the saved cards list (T12) under the payment\n * root, never nested inside either.** That placement is what lets this one CTA serve both the\n * new-card and saved-card paths — nesting it inside a card form, as the legacy `CardForm` does,\n * would force a second CTA for the saved-card path. This is a documented authoring convention, not a\n * compiler guarantee: `requiresContext: 'payment'` (ADR-0009) only fails the build when *no* ancestor\n * at any depth declares `providesContext: 'payment'` — there is no `forbidsAncestor` token, so a\n * schema that nests this component inside `CardEntry`/`SavedCards` instead of beside them still\n * compiles and still resolves the context from the payment root further up. This component throws\n * at runtime only when rendered entirely outside a `PaymentSectionRoot`/`Controller` subtree — both\n * `usePayment()` (read directly below, for `draft.billingAddressDifferent`) and `usePlaceOrder` (via\n * the submission channel those roots provide) enforce that guard, with `usePayment()` throwing first\n * since its value feeds `usePlaceOrder`'s own argument — a real, load-bearing guard, but a weaker one\n * than \"must be a sibling.\" See specs/place-order.md's open items for this gap.\n *\n * **This component never touches card data.** It declares its own thin fragment for the read-model\n * slice `usePlaceOrder` needs (`selectedMethodId`, `selectedInstallment`, `methods` — ADR-0011,\n * mirroring `PaymentSummaryData`/`PaymentSectionData`) and forwards it plus `$actions` straight into\n * the hook. The one fact it reads off `usePayment()`'s context directly is `draft.billingAddressDifferent`\n * (T11's widening of `PaymentDraft`), forwarded into `usePlaceOrder`'s own optional argument of the\n * same name so the gateway descriptor carries the shopper's choice (fix pass, PR #544 review F1) —\n * still not a card field, since `PaymentDraft` never carries one. `usePaymentSubmitGate()` (T24)\n * resolves the client-side document/billing refusal off that same context and is forwarded as\n * `usePlaceOrder`'s `submitGate` argument — an opaque `PaymentUserError | null`, never the facts\n * behind it. The hook already owns `placing`, `userErrors`, and `orderId` — including releasing\n * `placing` on every failure path — so this component only reflects that state, never re-implements it.\n *\n * `attempted` is the one bit of state this component adds, and it is not a duplicate of anything the\n * hook owns: `usePlaceOrder`'s `userErrors: []` is both the pre-attempt state and the settled-success\n * state (T13 does not disambiguate the two, by design — see `UsePlaceOrder`'s doc), so distinguishing\n * \"never attempted\" from \"attempted and succeeded\" is exactly this component's job. `succeeded` is\n * derived, not stored: `attempted && !placing && userErrors.length === 0`.\n *\n * **T29 widens both `succeeded` and the button label with the surviving `attempt` record read off\n * `usePayment()`** (specs/make-an-order-attempt-survive-a.md), never mirrored into local state:\n * `attempted`/`userErrors`/`orderId` all restart at their pre-attempt defaults on every mount, but the\n * attempt record does not, so a remount renders its truth rather than this instance's own fresh state.\n * `succeeded` is `true` whenever the record's own phase is `'settled'`, independent of `attempted`; the\n * button shows `resumeLabel` instead of `label` whenever the phase is `'sent'` or `'unresolved'` — the\n * card already left the browser (or its fate is unknown) and the next click finalizes it through\n * `usePlaceOrder`'s `resume()` rather than opening a new transaction.\n *\n * The button disables on `placing` (the required blocking state) and, deliberately, on `succeeded`\n * too — an already-placed order has nothing left to submit, and a second click risks exactly the\n * duplicate-transaction outcome T13's own concurrency guard (Decision 6) calls \"the worst outcome\n * this task's surface has\". `usePlaceOrder`'s in-flight ref is the inner guard; this is the outer one.\n * Fix pass, finding S-F1: it also disables on the *shared* `usePayment().placing`, since a second\n * composed `PlaceOrder` has no other way to learn a sibling instance already opened a transaction —\n * each `usePlaceOrder()` call owns its own, independent `inFlight` ref.\n */\n\n// ---------------------------------------------------------------------------\n// GraphQL Fragment — raw template literal, matching the merged `payment`/`payment-summary` siblings:\n// the codegen `documents` glob does not scan `src/matter/place-order*`, so the typed `graphql()`\n// factory is unavailable here; `data-component-contract.test.ts` validates this fragment against the\n// real schema and the built catalog.\n//\n// Selects exactly the three fields `PlaceOrderData` (sdk/payment/use-place-order.ts) needs — nothing\n// from `PaymentSectionData` is re-selected, and nothing card-shaped is ever in scope.\n// ---------------------------------------------------------------------------\n\nconst fragment = /* GraphQL */ `\n fragment PlaceOrderData on Query {\n payment {\n __typename\n id\n selectedMethodId\n selectedInstallment {\n __typename\n count\n value {\n amount\n currencyCode\n }\n interestRate\n }\n methods {\n __typename\n id\n groupName\n }\n }\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Props\n// ---------------------------------------------------------------------------\n\nexport type PlaceOrderProps = ComponentPropsWithRef<'div'> & {\n /** CTA label while idle. */\n label?: string;\n /** CTA label while the three-call sequence is in flight. */\n placingLabel?: string;\n /**\n * CTA label while a surviving attempt (T29) offers a resume — the card already left the browser\n * (`sent`) or its fate is unknown (`unresolved`), and the next click finalizes it rather than running\n * the full sequence again. Shown instead of `label`, never alongside `placingLabel` (mutually\n * exclusive — `placing` takes priority whenever both could apply).\n */\n resumeLabel?: string;\n /** Inline message shown once the sequence has settled with no `userErrors`. */\n successMessage?: string;\n /** Shown when `placeOrder()` is invoked with no registered submission for the current path. */\n missingSubmissionMessage?: string;\n /** Shown when no payment method is selected yet. */\n missingMethodMessage?: string;\n /** Shown when `startTransaction` resolves incomplete transaction data. */\n invalidTransactionMessage?: string;\n /** Shown for an infrastructure-level failure reaching the payment gateway action. */\n networkErrorMessage?: string;\n /** Shown when the selected method requires a cardholder document and none is set. */\n missingDocumentMessage?: string;\n /** Shown when the typed cardholder document fails validation. */\n invalidDocumentMessage?: string;\n /**\n * Shown instead of the two document messages above when the selected method calls for a cardholder\n * document the page is not showing — a method the card form cannot pay with, or one outside the\n * section's `group` — so the shopper has to choose another payment method (T192).\n */\n unavailableMethodMessage?: string;\n /** Shown when the shopper declined \"same as delivery\" but never saved a billing address. */\n unsavedBillingAddressMessage?: string;\n /** Shown when the new-card draft itself fails validation before ever reaching the gateway. */\n invalidCardDataMessage?: string;\n /** Shown when a saved-card submission is refused for a missing or invalid CVV. */\n invalidSecurityCodeMessage?: string;\n /** Shown when the payment gateway itself refuses the submitted card. */\n gatewayErrorMessage?: string;\n /**\n * Shown when a click lands on an order-placement attempt whose own sequence is still running — a\n * transaction this instance did not open, and which nothing here may finalize or re-run (T170).\n */\n inFlightMessage?: string;\n /**\n * Shown when the payment was handed to the gateway but the order could not be confirmed, so it may still be\n * processing (T176). Rendered as a status, never as the error alert.\n */\n unconfirmedMessage?: string;\n /**\n * Route template navigated to after a successful placement. The literal `{orderGroup}` is\n * replaced by the placed order's group, percent-encoded. Absent ⇒ no navigation. Must resolve to\n * the current page's origin — an absolute, protocol-relative, or non-http(s) template is refused\n * rather than navigated to (fail closed, dev-only warn; fix pass, finding S1).\n * e.g. \"/reserva/checkout/order-placed/{orderGroup}\"\n */\n successHref?: string;\n /** `Payment` slice forwarded by the reactive runtime (arity-1, ADR-0015) — `null` until it resolves. */\n $data?: PlaceOrderData;\n /** Compiler-injected action surface; carries `payment.startTransaction`, `payment.gatewayCallback`, `analytics.track`. */\n $actions?: ComponentActions;\n};\n\n// ---------------------------------------------------------------------------\n// Definition\n// ---------------------------------------------------------------------------\n\nexport const PlaceOrderDefinition = defineComponent<PlaceOrderProps>({\n name: 'PlaceOrder',\n importPath: '@vtex/faststore/components',\n component: PlaceOrder,\n fragment,\n fetchStrategy: 'reactive',\n actions: { payment: ['startTransaction', 'gatewayCallback'], analytics: ['track'] },\n requiresContext: 'payment',\n editor: { label: 'Place Order', category: 'commerce' },\n schema: {\n // Literals, not shared constants: the native catalog builder extracts `schema` statically and\n // silently drops an identifier-valued default (place-order.test.tsx asserts the built catalog\n // keeps every one of them, mirroring payment.tsx's group-default guard).\n label: { type: 'string', default: 'Place order', label: 'Button label' },\n placingLabel: { type: 'string', default: 'Placing order...', label: 'Button label while placing' },\n resumeLabel: {\n type: 'string',\n default: 'Complete your order',\n label: 'Button label while a surviving attempt offers a resume',\n },\n successMessage: { type: 'string', default: 'Order placed successfully.', label: 'Success message' },\n // T31 (specs/make-the-payment-components-authored-copy.md, FR-4, Decision 1) — one prop per\n // client-synthesized PaymentUserError code this CTA's alert can render, each defaulting to that\n // error's current, unchanged `message` (Decision 11 — the module-level constants that produce\n // these codes elsewhere in the payment surface keep their own message verbatim).\n missingSubmissionMessage: {\n type: 'string',\n default: 'Enter your payment details before placing the order.',\n label: 'Missing payment submission message',\n },\n missingMethodMessage: {\n type: 'string',\n default: 'Choose a payment method before placing the order.',\n label: 'Missing payment method message',\n },\n invalidTransactionMessage: {\n type: 'string',\n default: 'Unable to start the payment. Please try again.',\n label: 'Invalid transaction message',\n },\n networkErrorMessage: {\n type: 'string',\n default: 'Unable to complete the request. Please try again.',\n label: 'Network error message',\n },\n missingDocumentMessage: {\n type: 'string',\n default: 'Enter your document before placing the order.',\n label: 'Missing document message',\n },\n invalidDocumentMessage: {\n type: 'string',\n default: 'Enter a valid document before placing the order.',\n label: 'Invalid document message',\n },\n unavailableMethodMessage: {\n type: 'string',\n default: 'Choose another payment method before placing the order.',\n label: 'Unavailable payment method message',\n },\n unsavedBillingAddressMessage: {\n type: 'string',\n default: 'Save your billing address before placing the order.',\n label: 'Unsaved billing address message',\n },\n invalidCardDataMessage: {\n type: 'string',\n default: 'Enter valid card details before placing the order.',\n label: 'Invalid card data message',\n },\n invalidSecurityCodeMessage: {\n type: 'string',\n default: 'Enter a valid security code before placing the order.',\n label: 'Invalid security code message',\n },\n gatewayErrorMessage: {\n type: 'string',\n default: 'Unable to complete the payment. Please try again.',\n label: 'Gateway error message',\n },\n inFlightMessage: {\n type: 'string',\n default: 'This order is already being placed. Please wait.',\n label: 'Order already in flight message',\n },\n unconfirmedMessage: {\n type: 'string',\n default: 'We could not confirm your order yet. Your payment may still be processing, so please do not pay again.',\n label: 'Unconfirmed order message',\n },\n // No default (FR-2, T43, deliberate — see Decision 3 in specs/navigate-the-shopper-to-the-order.md):\n // absence of this prop must mean absence of the navigation effect, so a default here would silently\n // start redirecting every existing composition. Says \"token\", not the spec's own word for this —\n // see the note above warnMissingOrderGroupToken below.\n successHref: { type: 'string', label: 'Confirmation route template ({orderGroup} token)' },\n },\n guideline:\n \"The place-order CTA: invokes usePlaceOrder's three-call sequence (start transaction, the registered submission's own direct-to-gateway leg, then the gateway callback) and blocks interaction while it runs. Compose it as a sibling — never a child — of every other payment-entry leaf under the same PaymentSectionRoot or PaymentSectionController, so the one CTA serves every entry path. Renders one inline outcome after settling: the first userError's message, or successMessage on a clean settle. This component reaches no payment-sensitive input. Must be inside a payment root. See place-order.md.\",\n});\n\n// ---------------------------------------------------------------------------\n// Implementation\n// ---------------------------------------------------------------------------\n\n/**\n * OQ-6 (2026-08-12 spec gate, `specs/navigate-the-shopper-to-the-order.md`): a `successHref` missing\n * the `{orderGroup}` placeholder is a store-authoring mistake — every order would land on the same\n * literal URL, which then renders unresolved. Fail closed instead of navigating there, and warn in\n * development only, mirroring `variant-selector.tsx`'s `warnUnresolvedAxis`.\n *\n * Named and worded around \"token\" rather than the spec's own word for this: that word contains\n * `holder` as a substring (the same reason `cardholder` matches), which trips `CARD_FIELD_PATTERN`\n * (`card-field-pattern.ts`) — the pattern `place-order.test.tsx`'s \"card-data boundary\" suite asserts\n * never matches this file's source text.\n */\nfunction warnMissingOrderGroupToken(successHref: string): void {\n if (process.env.NODE_ENV === 'production') return;\n console.warn(\n `[PlaceOrder] successHref \"${successHref}\" has no {orderGroup} token; refusing to navigate there ` +\n 'rather than sending every order to the same URL.',\n );\n}\n\n/**\n * Fix pass, finding S1 (High, CWE-601 open redirect): nothing upstream validates `successHref`'s\n * shape — the schema field is a bare `{type: 'string'}` with no pattern/format/enum, sukima never\n * inspects prop values, and this runtime's only prior check was the `{orderGroup}` substring test\n * above, which an absolute or protocol-relative template still satisfies\n * (`\"https://evil.example/{orderGroup}\"`, `\"//evil.example/{orderGroup}\"`). Left unchecked, a store\n * schema can turn this CTA into an open redirect that fires immediately after a real purchase,\n * carrying the shopper's own order group to an attacker-controlled origin.\n *\n * Resolves the template against the current origin with the same parser Next.js's own router applies\n * internally to decide \"external\" (`dispatchNavigateAction` -> `new URL(href, location.href)` ->\n * `isExternalURL`), rather than a `startsWith('/')` string check: a prefix check alone is bypassed by\n * a leading backslash or an embedded control character — `new URL('/\\\\evil.example', origin)` and\n * `new URL('/\\t/evil.example', origin)` both resolve to the `evil.example` origin under the WHATWG URL\n * parser (the same parser the browser itself applies before navigating) while starting with a single\n * `/` and not `//`. Asking the parser the same question it is asked again downstream is what makes\n * this bypass-proof; it also rejects `javascript:` and other non-http(s) schemes for free, since their\n * `.origin` is the literal string `\"null\"`, which never equals a real page origin (finding S2).\n */\nfunction isSameOriginTemplate(successHref: string): boolean {\n try {\n return new URL(successHref, window.location.origin).origin === window.location.origin;\n } catch {\n return false;\n }\n}\n\n/** Fail-closed counterpart of `warnMissingOrderGroupToken`, for a `successHref` that resolves off the\n * current origin (finding S1) — same dev-only, non-production channel. */\nfunction warnUnsafeSuccessHref(successHref: string): void {\n if (process.env.NODE_ENV === 'production') return;\n console.warn(\n `[PlaceOrder] successHref \"${successHref}\" resolves to a different origin than the current page; ` +\n 'refusing to navigate there rather than sending the shopper off-site right after a purchase.',\n );\n}\n\ntype PlaceOrderNavigateOnSuccessProps = Pick<UsePlaceOrder, 'orderId' | 'userErrors'> & {\n successHref: string;\n};\n\n/**\n * Fix pass, finding R1 (High): `useRouter()` requires an App Router context, and `PlaceOrder` used to\n * call it unconditionally — so the component could not render at all outside one, even for every\n * composition that never sets `successHref` (`place-order.stories.tsx`'s four stories, which mount\n * under no such context, were the concrete in-repo victim). Splitting the hook into its own child,\n * rendered only while `successHref` is set (see the call site below), means `useRouter()` — and the\n * App Router requirement that comes with it — is invoked only once a store has actually opted into the\n * navigation. This restores INV-4 / NFR-1 / US-3 AC1: with `successHref` absent, `PlaceOrder` neither\n * renders this child nor calls `useRouter()` at all.\n *\n * The effect is otherwise T43's original: navigates once `orderId` transitions to a non-empty value —\n * not on `succeeded` (Decision 4) — so a null or empty order group (fix pass, finding R4) and every\n * failure path never navigate. `userErrors` is also consulted directly (fix pass, finding R3 / INV-1):\n * today's real `usePlaceOrder` never settles a non-empty `orderId` alongside a non-empty `userErrors`,\n * but this component does not own that hook and should not rely on an invariant of it that T29 is\n * about to rework (spec Decision 1) — checking both fields here keeps INV-1 true by this component's\n * own construction, not by inheritance from a hook it does not control.\n *\n * Fix pass (Low finding, T29): the call site below passes `displayOrderId` (`orderId ?? the surviving\n * attempt's own orderId`), not the mount-local `orderId` this type is named for — a remounted CTA\n * whose own `usePlaceOrder()` never itself ran (it read an already-`settled` attempt straight from\n * `usePayment()`) has an empty local `orderId` forever, so passing the local value here left a store\n * whose only confirmation surface is `successHref` stuck on the payment page after exactly the remount\n * this task exists to make safe: `succeeded` rendered (from the shared attempt) but `router.replace`\n * never fired (from the mount-local, always-null `orderId`).\n */\nfunction PlaceOrderNavigateOnSuccess({ successHref, orderId, userErrors }: PlaceOrderNavigateOnSuccessProps) {\n const router = useRouter();\n\n useEffect(() => {\n if (!orderId || userErrors.length > 0) return;\n if (!isSameOriginTemplate(successHref)) {\n warnUnsafeSuccessHref(successHref);\n return;\n }\n if (!successHref.includes('{orderGroup}')) {\n warnMissingOrderGroupToken(successHref);\n return;\n }\n // `replaceAll`, not `replace` (fix pass, finding R5): a template with the placeholder repeated —\n // e.g. a route segment plus a tracking query param — passes the `includes` guard above on its\n // first occurrence alone, and `String.prototype.replace` with a string pattern substitutes only\n // that first occurrence, leaving every later `{orderGroup}` in the URL literal and unencoded.\n router.replace(successHref.replaceAll('{orderGroup}', encodeURIComponent(orderId)));\n }, [orderId, successHref, userErrors, router]);\n\n return null;\n}\n\n/** The message props FR-4 adds, unnested — one field per client-synthesized `PaymentUserError` code\n * this CTA can render. Ten from T31; `inFlightMessage` joins them for T170's own code,\n * `unconfirmedMessage` for T176's, which renders in the status node instead of the alert, and\n * `unavailableMethodMessage` for T192's. */\nexport type AuthoredPlaceOrderMessages = Readonly<{\n missingSubmissionMessage: string;\n missingMethodMessage: string;\n invalidTransactionMessage: string;\n networkErrorMessage: string;\n missingDocumentMessage: string;\n invalidDocumentMessage: string;\n unsavedBillingAddressMessage: string;\n invalidCardDataMessage: string;\n invalidSecurityCodeMessage: string;\n gatewayErrorMessage: string;\n inFlightMessage: string;\n unconfirmedMessage: string;\n unavailableMethodMessage: string;\n}>;\n\n/**\n * Builds the code -> copy map `resolveErrorMessage` reads (Decision 1, FR-4). The codes are literal\n * strings, never imported constants: importing them from card-entry.tsx / saved-cards.tsx /\n * use-place-order.ts / use-payment-submit-gate.ts / gateway.ts here would make this file a matter ->\n * matter dependency (R5) — `resolvePaymentSubmitGate` and `use-place-order.ts` stay untouched and this\n * component still imports nothing from either module beyond the hooks it already called. Exported for\n * direct unit testing, mirroring card-entry.tsx's convention for its own pure helpers:\n * place-order.test.tsx imports the five files' own real exported code constants and asserts this\n * function's returned key set equals their union.\n */\nexport function buildAuthoredErrorMessages(messages: AuthoredPlaceOrderMessages): Readonly<Record<string, string>> {\n return {\n MISSING_PAYMENT_SUBMISSION: messages.missingSubmissionMessage,\n MISSING_PAYMENT_METHOD_SELECTION: messages.missingMethodMessage,\n INVALID_TRANSACTION_DATA: messages.invalidTransactionMessage,\n NETWORK_ERROR: messages.networkErrorMessage,\n MISSING_CARDHOLDER_DOCUMENT: messages.missingDocumentMessage,\n INVALID_CARDHOLDER_DOCUMENT: messages.invalidDocumentMessage,\n UNSAVED_BILLING_ADDRESS: messages.unsavedBillingAddressMessage,\n INVALID_CARD_DATA: messages.invalidCardDataMessage,\n INVALID_SECURITY_CODE: messages.invalidSecurityCodeMessage,\n GATEWAY_ERROR: messages.gatewayErrorMessage,\n PLACE_ORDER_IN_FLIGHT: messages.inFlightMessage,\n PLACE_ORDER_UNCONFIRMED: messages.unconfirmedMessage,\n UNAVAILABLE_PAYMENT_METHOD: messages.unavailableMethodMessage,\n };\n}\n\n/**\n * Resolves the alert's rendered message for one client-synthesized `PaymentUserError`: the authored\n * copy for its `code` when this component owns one, otherwise the error's own `message` unchanged\n * (US-2 AC2 — a server-returned code this component does not author must render verbatim).\n *\n * `||`, not `??` (fix pass, finding 7): a store author can write `\"\"` for a code's authored message —\n * a valid, if unusual, `type: 'string'` prop value — and `??` only falls through on `null`/`undefined`,\n * so an authored empty string would render as an empty `role=\"alert\"` on a genuine gateway refusal,\n * leaving the shopper with no feedback at all where the unauthored default always rendered\n * `error.message`. Falling through on any falsy authored value is the exception on this surface, not\n * the rule: eight sibling `??` resolutions elsewhere (`access-code-sign-in.tsx`, `region.tsx`) stay on\n * `??`; `billing-address.tsx`'s `fieldLabels` fallback (fix pass round 2, finding N1) is fixed the\n * same way, for the same reason.\n */\nexport function resolveErrorMessage(error: PaymentUserError, authored: Readonly<Record<string, string>>): string {\n return authored[error.code] || error.message;\n}\n\nexport function PlaceOrder({\n label = 'Place order',\n placingLabel = 'Placing order...',\n resumeLabel = 'Complete your order',\n successMessage = 'Order placed successfully.',\n missingSubmissionMessage = 'Enter your payment details before placing the order.',\n missingMethodMessage = 'Choose a payment method before placing the order.',\n invalidTransactionMessage = 'Unable to start the payment. Please try again.',\n networkErrorMessage = 'Unable to complete the request. Please try again.',\n missingDocumentMessage = 'Enter your document before placing the order.',\n invalidDocumentMessage = 'Enter a valid document before placing the order.',\n unavailableMethodMessage = 'Choose another payment method before placing the order.',\n unsavedBillingAddressMessage = 'Save your billing address before placing the order.',\n invalidCardDataMessage = 'Enter valid card details before placing the order.',\n invalidSecurityCodeMessage = 'Enter a valid security code before placing the order.',\n gatewayErrorMessage = 'Unable to complete the payment. Please try again.',\n inFlightMessage = 'This order is already being placed. Please wait.',\n unconfirmedMessage = 'We could not confirm your order yet. Your payment may still be processing, so please do not pay again.',\n successHref,\n $data: $dataProp,\n $actions: $actionsProp,\n className,\n ...props\n}: PlaceOrderProps) {\n const $data = useDataOverride($dataProp);\n const $actions = useActionsOverride($actionsProp);\n // T28: this instance's own stable identity, so its publish effect below retracts only its own key\n // (Decision 4) — never a second composed PlaceOrder's still-in-flight one.\n const instanceKey = useId();\n // Also enforces the PaymentSectionRoot/PaymentSectionController ancestor: usePayment() throws\n // outside that subtree (see the module doc comment) — the first of the two guards to run, since its\n // result feeds usePlaceOrder's own argument below.\n //\n // Fix pass, finding S-F1 (High, duplicate-transaction route (a)): `sectionPlacing` is the shared\n // fact this instance also *writes* below — read back so this CTA's own `disabled` can OR it in.\n // Without this, a second composed PlaceOrder stayed enabled for the whole time a first instance's\n // sequence was open: each instance's `usePlaceOrder()` call owns its own independent `placing`/\n // `inFlight` guard, so nothing local to a second CTA ever knew a sibling had already opened a\n // transaction. This never makes this CTA's own subtree inert (FR-6/Decision 5 are untouched) — it\n // only widens the *button's* own disabled condition.\n //\n // T29: `attempt`/`setAttempt` are this same contract's own record of an order-placement attempt —\n // read here (never mirrored locally) so a remount renders the surviving truth instead of this\n // instance's own fresh, reset local state (see `succeeded`/`resumable` below).\n const { draft, placing: sectionPlacing, setPlacing, attempt, setAttempt } = usePayment();\n // T24's submit gate: an already-resolved client-side refusal (a missing/malformed cardholder\n // document, or an unsaved billing address), forwarded verbatim into usePlaceOrder's own opaque\n // argument — still not a card field, since the hook only ever receives the resolved verdict.\n const submitGate = usePaymentSubmitGate();\n const { placeOrder, placing, userErrors, orderId } = usePlaceOrder({\n $data,\n $actions,\n attempt,\n setAttempt,\n billingAddressDifferent: draft.billingAddressDifferent,\n submitGate,\n });\n const authoredErrorMessages = buildAuthoredErrorMessages({\n missingSubmissionMessage,\n missingMethodMessage,\n invalidTransactionMessage,\n networkErrorMessage,\n missingDocumentMessage,\n invalidDocumentMessage,\n unsavedBillingAddressMessage,\n invalidCardDataMessage,\n invalidSecurityCodeMessage,\n gatewayErrorMessage,\n inFlightMessage,\n unconfirmedMessage,\n unavailableMethodMessage,\n });\n const [attempted, setAttempted] = useState(false);\n\n // T28 (FR-1/FR-7): mirrors this CTA's own in-flight state onto the payment context so the sibling\n // entry leaves can freeze while an order-placement sequence is open. Retracts only this instance's\n // key on unmount, so a second composed PlaceOrder's own live sequence is never unblocked by this one\n // settling or unmounting (Decision 4, R5).\n //\n // Fix pass, finding F7 (assumption A-3): a *layout* effect, not a passive one — it must commit in\n // the same frame as `placing`'s own render, so no intermediate paint ever shows this CTA's button\n // still enabled beside leaves that are about to go inert (or the reverse, on release). A passive\n // `useEffect` here leaves a ~1-frame window where a fast key-repeat could land.\n //\n // T29 (FR-5, INV-5): the cleanup's own release is now conditional on `placing` — closed over from\n // the same render as this effect instance, so it reflects this CTA's own sequence at the moment it\n // unmounts. `placing` is true for the whole span from before `startTransaction()`/`gatewayCallback()`\n // to `run()`/`resume()` settling (unchanged by this task), so an unmount while it is still true is\n // exactly an unmount mid-sequence (the overlay closing with no `keepMounted`) — the `run()`/`resume()`\n // closure this CTA started keeps writing the surviving attempt's phase after this CTA is gone\n // (NFR-3), and nothing else will ever flip this key back, so the release must not happen. A routine\n // re-render where `placing` itself just flipped self-corrects immediately after, via this same\n // effect's next body call, which re-publishes the current value directly.\n //\n // Fix pass (Critical finding): this instance's own key in `usePayment()`'s ref-counted set is\n // therefore genuinely never retracted once abandoned here — by design, since nothing can safely\n // predict at unmount time whether the closure is about to resolve to a terminal phase or is still\n // truly in flight. What bounds the damage is `usePaymentMethodSelection`'s own `placing` derivation,\n // which folds the shared attempt's phase into every reader of this fact: once the abandoned closure\n // finally writes `refused` or `settled`, `placing` reads `false` again for this root regardless of\n // how many stale keys the ref-counted set still carries. `blockedByOtherInstance`/`resumable` below\n // read that same, already-corrected `sectionPlacing` value, so they need no separate escape hatch of\n // their own for this case.\n useLayoutEffect(() => {\n setPlacing(instanceKey, placing);\n return () => {\n if (placing) return;\n setPlacing(instanceKey, false);\n };\n }, [instanceKey, placing, setPlacing]);\n\n const [firstError] = userErrors;\n // T29: a settled attempt renders as succeeded even across a remount, where this component's own\n // `attempted`/`userErrors` restart at their pre-attempt defaults (FR-1) but the shared `attempt`\n // does not. On the same mount, `attempted && !placing && userErrors.length === 0` already covers it\n // (both a clean `run()` and a clean `resume()` settle through the same wrapper), so this only adds\n // the case local state cannot see.\n const settledAttemptOrderId = attempt && attempt.phase === 'settled' ? attempt.orderId : null;\n const succeeded = (attempted && !placing && userErrors.length === 0) || attempt?.phase === 'settled';\n // T29 (FR-2, FR-3): the next click finalizes rather than re-runs the sequence. Never true once\n // `succeeded` is (phases are mutually exclusive), and `placing` already takes priority in the label\n // below, so this needs no exclusion of its own here. The `'sent'` check is defensive-only today: no\n // write site in `run()` rests there (it writes `finalizing` directly, same tick, specs/make-an-order-\n // attempt-survive-a.md §2) — kept in case a future write path ever rests there again, rather than\n // silently treating that phase as non-resumable.\n const resumable = attempt?.phase === 'sent' || attempt?.phase === 'unresolved';\n // Local `orderId` (this mount's own settle) is preferred; a remounted CTA reading a settled attempt\n // has none of its own yet, so it falls back to the record's.\n const displayOrderId = orderId ?? settledAttemptOrderId;\n // T176 (FR-10, INV-11): the card was handed to the gateway and the order could not be confirmed, so\n // the payment may still be processing. That is a pending state, not a refusal: it renders in its own\n // status node, never in the alert stores style as an error, and never beside a success line — another\n // composed CTA can settle the shared record while this one still holds the outcome.\n const unconfirmed = firstError?.code === 'PLACE_ORDER_UNCONFIRMED';\n\n // T29: `sectionPlacing` overrides `resumable` below, not the other way round — a CTA that unmounted\n // mid-sequence (FR-5) leaves its own key published forever, because the `run()`/`resume()` closure it\n // abandoned settles through *local* state (`setPlacing(false)` inside `usePlaceOrder`'s own `settle`)\n // that has nowhere left to render once that CTA is gone, so the shared key it once set is never\n // retracted by anything else either. A resumable attempt must still be reachable through that stale\n // lock: `resume()` cannot re-pay by construction (Decision 3), so overriding a stuck `sectionPlacing`\n // here is safe exactly when it matters — a lock a *different*, still-genuinely-placing instance holds\n // is never resumable at the same time (the shared attempt is one fact — Decision 1 — so a real\n // concurrent opener keeps this attempt at `opening`/`finalizing`, not `sent`/`unresolved`).\n const blockedByOtherInstance = sectionPlacing && !resumable;\n\n function handleClick() {\n // FR-2 (T56): `setAttempted(true)` moved off the click and onto settlement, so `succeeded`\n // means \"an attempt settled with no refusals\" rather than \"a click happened and nothing is\n // in flight\". `placeOrder()` resolves; it never rejects (T13's own contract), and this\n // component reflects its state from the hook's own return value on every render rather than\n // from the awaited result — a rejection handler here would be dead code, and (review finding\n // F3) setting `attempted` from one would not \"keep the CTA disabled\": it would make\n // `succeeded` `true` with the attempt's real outcome unknown, the exact false-success frame\n // FR-2 exists to prevent.\n //\n // Review finding F1: settling `attempted` is not enough on its own past the first attempt.\n // Once an attempt settles, `attempted` stays `true` until something lowers it — and nothing\n // did, so a second click's own synchronous commit (`placeOrder` resets `userErrors` to `[]`\n // unconditionally, and a refusal leaves `placing` `false`) satisfied `succeeded` immediately,\n // one microtask before the new refusal arrived. Resetting `attempted` here, in the same\n // synchronous batch as that reset, makes every attempt — not just the first — start its own\n // click commit from \"not attempted\"; the `.then` below raises it again only once *this*\n // attempt's own outcome is in.\n setAttempted(false);\n void placeOrder().then(() => setAttempted(true));\n }\n\n return (\n <div\n {...props}\n className={className}\n data-fs-place-order=\"\"\n {...(placing ? { 'data-fs-place-order-pending': '' } : {})}\n >\n <Button\n onClick={handleClick}\n disabled={placing || succeeded || blockedByOtherInstance}\n aria-busy={placing || undefined}\n data-fs-place-order-button=\"\"\n {...(placing ? { 'data-fs-place-order-button-pending': '' } : {})}\n >\n {placing ? placingLabel : resumable ? resumeLabel : label}\n </Button>\n {firstError && !unconfirmed ? (\n <div role=\"alert\" data-fs-place-order-error=\"\">\n {resolveErrorMessage(firstError, authoredErrorMessages)}\n </div>\n ) : null}\n {firstError && unconfirmed && !succeeded ? (\n <div role=\"status\" data-fs-place-order-unconfirmed=\"\">\n {resolveErrorMessage(firstError, authoredErrorMessages)}\n </div>\n ) : null}\n {succeeded ? (\n <div role=\"status\" data-fs-place-order-success=\"\">\n <span data-fs-place-order-success-message=\"\">{successMessage}</span>\n {displayOrderId ? <span data-fs-place-order-success-order-id=\"\">{displayOrderId}</span> : null}\n </div>\n ) : null}\n {successHref != null ? (\n <PlaceOrderNavigateOnSuccess successHref={successHref} orderId={displayOrderId} userErrors={userErrors} />\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"product-list.js","names":[],"sources":["../../../src/matter/product-list/product-list.tsx"],"sourcesContent":["'use client';\n\nimport { defineComponent } from '@vtex/fs-core';\nimport { type ComponentPropsWithRef, createContext, useContext, useMemo } from 'react';\nimport { Badge } from '../../particles/badge/badge.js';\nimport { Button } from '../../particles/button/button.js';\nimport { Container } from '../../particles/container/container.js';\nimport { Image } from '../../particles/image/image.js';\nimport { Text } from '../../particles/text/text.js';\nimport { useActionsOverride } from '../../sdk/actions/actions-override-context.js';\nimport type { ComponentActions } from '../../sdk/actions/types.js';\nimport { useStorefrontContext } from '../../sdk/context/storefront-context.js';\n\n// ---------------------------------------------------------------------------\n// Product list config context (tax display)\n// ---------------------------------------------------------------------------\n\ntype ProductListConfig = {\n usePriceWithTaxes: boolean;\n taxesLabel: string;\n};\n\nexport const ProductListConfigContext = createContext<ProductListConfig>({\n usePriceWithTaxes: false,\n taxesLabel: 'Tax included',\n});\n\nfunction useProductListConfig(): ProductListConfig {\n return useContext(ProductListConfigContext);\n}\n\nfunction formatPrice(value: number, locale: string, currency: string): string {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value);\n}\n\n// Tax rate for the item: the explicit `taxPercentage` (e.g. 0.18) wins; otherwise it is derived\n// from the absolute tax amount over the price. Missing/zero-price data yields 0 (no change).\nfunction taxRate(item: ProductListItem): number {\n if (item.taxPercentage != null) {\n return item.taxPercentage;\n }\n if (item.tax != null && item.price > 0) {\n return item.tax / item.price;\n }\n return 0;\n}\n\nfunction priceWithTaxes(base: number, item: ProductListItem): number {\n return base * (1 + taxRate(item));\n}\n\n// ---------------------------------------------------------------------------\n// Product list item types\n// ---------------------------------------------------------------------------\n\nexport type ProductListItemImage = {\n src: string;\n alt: string;\n};\n\nexport type ProductListItem = {\n id: string;\n name: string;\n image: ProductListItemImage;\n price: number;\n listPrice?: number;\n variant?: string;\n slug?: string;\n // Widened for the Product Shelf (T2): fields the card parts T3–T7 read straight off the\n // per-item context. Optional so the plain-`products` ProductListRoot (PLP) stays valid.\n spotPrice?: number;\n tax?: number;\n taxPercentage?: number | null;\n availableForSale?: boolean;\n availableQuantity?: number;\n currency?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Products context (provided by Root, consumed by Grid)\n// ---------------------------------------------------------------------------\n\nconst ProductListProductsContext = createContext<ProductListItem[]>([]);\n\n// ---------------------------------------------------------------------------\n// Item context (provided by Grid per product)\n// ---------------------------------------------------------------------------\n\nexport const ProductListItemContext = createContext<ProductListItem | null>(null);\n\nfunction useProductListItem(): ProductListItem {\n const item = useContext(ProductListItemContext);\n if (!item) {\n throw new Error('ProductCard components must be used inside ProductListGrid.');\n }\n return item;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type ProductListRootProps = ComponentPropsWithRef<'div'> & {\n products?: ProductListItem[];\n /** When true, card prices render tax-inclusive (from the item's tax data). Default false. */\n usePriceWithTaxes?: boolean;\n /** Text for the tax label part when tax display is on. Default `'Tax included'`. */\n taxesLabel?: string;\n};\n\nexport type ProductListGridProps = ComponentPropsWithRef<'div'>;\n\nexport type ProductCardProps = ComponentPropsWithRef<'div'> & {\n /** Toggles the card border. Reflects onto data-fs-product-card-bordered; a swatch draws the border. Default true. */\n bordered?: boolean;\n};\n\nexport type ProductCardImageProps = ComponentPropsWithRef<'img'> & {\n objectFit?: 'contain' | 'cover' | 'fill' | 'none';\n /** CSS `aspect-ratio` (e.g. `'1/1'`, `'3/4'`, `'16/9'`); lays out the image box, orthogonal to `objectFit`. */\n aspectRatio?: string;\n};\n\nexport type ProductCardNameProps = ComponentPropsWithRef<'span'>;\n\nexport type ProductCardPriceProps = ComponentPropsWithRef<'span'>;\n\nexport type ProductCardListPriceProps = ComponentPropsWithRef<'span'>;\n\nexport type ProductCardTaxLabelProps = ComponentPropsWithRef<'span'>;\n\nexport type ProductCardDiscountProps = ComponentPropsWithRef<'span'> & {\n format?: 'percentage' | 'absolute';\n};\n\nexport type ProductCardAddToCartProps = ComponentPropsWithRef<'button'> & {\n /** Injected by the compiler from `actions: { cart: ['addItem'] }`; the cart write surface. */\n $actions?: ComponentActions;\n};\n\n// ---------------------------------------------------------------------------\n// Definitions\n// ---------------------------------------------------------------------------\n\nexport const ProductListRootDefinition = defineComponent<ProductListRootProps>({\n name: 'ProductListRoot',\n importPath: '@vtex/faststore/components',\n component: ProductListRoot,\n editor: { label: 'Product List Root', category: 'commerce' },\n schema: {\n usePriceWithTaxes: { type: 'boolean', default: false, label: 'Use Price With Taxes' },\n taxesLabel: { type: 'string', default: 'Tax included', label: 'Taxes Label' },\n },\n guideline:\n 'Top-level wrapper for the product list; compose as ProductListRoot > ProductListGrid > ProductCard* children. Set usePriceWithTaxes to render tax-inclusive card prices and compose ProductCardTaxLabel to show the taxesLabel.',\n});\n\nexport const ProductListGridDefinition = defineComponent<ProductListGridProps>({\n name: 'ProductListGrid',\n importPath: '@vtex/faststore/components',\n component: ProductListGrid,\n editor: { label: 'Product List Grid', category: 'commerce' },\n schema: {},\n guideline:\n 'Repeating grid that renders the ProductCard* template for each product; must be a direct child of ProductListRoot.',\n});\n\nexport const ProductCardDefinition = defineComponent<ProductCardProps>({\n name: 'ProductCard',\n importPath: '@vtex/faststore/components',\n component: ProductCard,\n editor: { label: 'Product Card', category: 'commerce' },\n schema: {\n bordered: { type: 'boolean', default: true, label: 'Bordered' },\n },\n guideline:\n 'Optional card wrapper for the ProductCard* parts; always emits data-fs-product-card and data-fs-product-card-bordered (true/false, from the bordered toggle), and adds data-fs-product-card-out-of-stock when the product is unavailable (derived from availability, no prop). The border look is a swatch keyed off data-fs-product-card-bordered. Renders all children unchanged. Must be inside ProductListGrid / ProductShelfItems.',\n});\n\nexport const ProductCardImageDefinition = defineComponent<ProductCardImageProps>({\n name: 'ProductCardImage',\n importPath: '@vtex/faststore/components',\n component: ProductCardImage,\n editor: { label: 'Product Card Image', category: 'commerce' },\n schema: {\n objectFit: { type: ['contain', 'cover', 'fill', 'none'] as const, default: 'cover', label: 'Object Fit' },\n aspectRatio: { type: 'string', label: 'Aspect Ratio' },\n },\n guideline:\n 'Renders the product image with a configurable aspectRatio (e.g. 1/1, 3/4, 16/9) and objectFit. Alt text comes from the catalog. Must be inside ProductListGrid.',\n});\n\nexport const ProductCardNameDefinition = defineComponent<ProductCardNameProps>({\n name: 'ProductCardName',\n importPath: '@vtex/faststore/components',\n component: ProductCardName,\n editor: { label: 'Product Card Name', category: 'commerce' },\n schema: {},\n guideline: 'Renders the product name. Must be inside ProductListGrid.',\n});\n\nexport const ProductCardPriceDefinition = defineComponent<ProductCardPriceProps>({\n name: 'ProductCardPrice',\n importPath: '@vtex/faststore/components',\n component: ProductCardPrice,\n editor: { label: 'Product Card Price', category: 'commerce' },\n schema: {},\n guideline:\n 'Displays the selling price formatted from the active storefront context; renders tax-inclusive when usePriceWithTaxes is enabled. Must be inside ProductListGrid.',\n});\n\nexport const ProductCardListPriceDefinition = defineComponent<ProductCardListPriceProps>({\n name: 'ProductCardListPrice',\n importPath: '@vtex/faststore/components',\n component: ProductCardListPrice,\n editor: { label: 'Product Card List Price', category: 'commerce' },\n schema: {},\n guideline:\n 'Displays the original (strikethrough) list price when it exceeds the selling price; renders tax-inclusive when usePriceWithTaxes is enabled. Must be inside ProductListGrid.',\n});\n\nexport const ProductCardTaxLabelDefinition = defineComponent<ProductCardTaxLabelProps>({\n name: 'ProductCardTaxLabel',\n importPath: '@vtex/faststore/components',\n component: ProductCardTaxLabel,\n editor: { label: 'Product Card Tax Label', category: 'commerce' },\n schema: {},\n guideline:\n 'Tax label (e.g. \"Tax included\") shown beside the price. Renders only when the ProductListRoot/ProductShelfRoot config usePriceWithTaxes is enabled (state-driven, no toggle); the text comes from the taxesLabel config. Compose it to place the label, omit it to hide it. Must be inside ProductListGrid / ProductShelfItems.',\n});\n\nexport const ProductCardDiscountDefinition = defineComponent<ProductCardDiscountProps>({\n name: 'ProductCardDiscount',\n importPath: '@vtex/faststore/components',\n component: ProductCardDiscount,\n editor: { label: 'Product Card Discount', category: 'commerce' },\n schema: {\n format: { type: ['percentage', 'absolute'] as const, default: 'percentage', label: 'Discount Format' },\n },\n guideline:\n 'Discount badge for the current product; auto-computes the markdown from listPrice vs price and renders only when a discount exists. Compose it to show the badge, omit it to hide it (no toggle). Must be inside ProductListGrid / ProductShelfItems.',\n});\n\nexport const ProductCardAddToCartDefinition = defineComponent<ProductCardAddToCartProps>({\n name: 'ProductCardAddToCart',\n importPath: '@vtex/faststore/components',\n component: ProductCardAddToCart,\n editor: { label: 'Product Card Add to Cart', category: 'commerce' },\n schema: {},\n actions: { cart: ['addItem'] },\n guideline: 'Add-to-cart button for the current product card; must be inside ProductListGrid.',\n});\n\n// ---------------------------------------------------------------------------\n// Implementations\n// ---------------------------------------------------------------------------\n\nexport function ProductListRoot({\n usePriceWithTaxes = false,\n taxesLabel = 'Tax included',\n products = [],\n className,\n children,\n ...props\n}: ProductListRootProps) {\n const config = useMemo(() => ({ usePriceWithTaxes, taxesLabel }), [usePriceWithTaxes, taxesLabel]);\n\n return (\n <ProductListConfigContext value={config}>\n <ProductListProductsContext value={products}>\n <Container {...props} className={className} data-fs-product-list-root=\"\">\n {children}\n </Container>\n </ProductListProductsContext>\n </ProductListConfigContext>\n );\n}\n\nexport function ProductListGrid({ className, children, ...props }: ProductListGridProps) {\n const products = useContext(ProductListProductsContext);\n\n return (\n <Container {...props} className={className} data-fs-product-list-grid=\"\">\n {products.map((product) => (\n <ProductListItemContext key={product.id} value={product}>\n {children}\n </ProductListItemContext>\n ))}\n </Container>\n );\n}\n\nexport function ProductCard({ className, children, bordered = true, ...props }: ProductCardProps) {\n const item = useProductListItem();\n const outOfStock = item.availableForSale === false || item.availableQuantity === 0;\n\n return (\n <Container\n {...props}\n className={className}\n data-fs-product-card=\"\"\n data-fs-product-card-bordered={String(bordered)}\n data-fs-product-card-out-of-stock={outOfStock ? '' : undefined}\n >\n {children}\n </Container>\n );\n}\n\nexport function ProductCardImage({ className, objectFit = 'cover', aspectRatio, ...props }: ProductCardImageProps) {\n const item = useProductListItem();\n\n return (\n <Image\n {...props}\n src={item.image.src}\n alt={item.image.alt}\n aspectRatio={aspectRatio}\n className={className}\n style={{ objectFit, ...props.style }}\n data-fs-product-card-image=\"\"\n />\n );\n}\n\nexport function ProductCardName({ className, children, ...props }: ProductCardNameProps) {\n const item = useProductListItem();\n\n return (\n <Text {...props} as=\"span\" className={className} data-fs-product-card-name=\"\">\n {children ?? item.name}\n </Text>\n );\n}\n\nexport function ProductCardPrice({ className, children, ...props }: ProductCardPriceProps) {\n const item = useProductListItem();\n const { usePriceWithTaxes } = useProductListConfig();\n const { locale, currency } = useStorefrontContext();\n const value = usePriceWithTaxes ? priceWithTaxes(item.price, item) : item.price;\n\n return (\n <Text {...props} as=\"span\" className={className} data-fs-product-card-price=\"\">\n {children ?? formatPrice(value, locale, item.currency ?? currency)}\n </Text>\n );\n}\n\nexport function ProductCardListPrice({ className, children, ...props }: ProductCardListPriceProps) {\n const item = useProductListItem();\n const { usePriceWithTaxes } = useProductListConfig();\n const { locale, currency } = useStorefrontContext();\n\n if (!item.listPrice || item.listPrice <= item.price) {\n return null;\n }\n\n const value = usePriceWithTaxes ? priceWithTaxes(item.listPrice, item) : item.listPrice;\n\n return (\n <Text {...props} as=\"span\" className={className} data-fs-product-card-list-price=\"\">\n {children ?? formatPrice(value, locale, item.currency ?? currency)}\n </Text>\n );\n}\n\nexport function ProductCardTaxLabel({ className, children, ...props }: ProductCardTaxLabelProps) {\n const { usePriceWithTaxes, taxesLabel } = useProductListConfig();\n\n if (!usePriceWithTaxes) {\n return null;\n }\n\n return (\n <Text {...props} as=\"span\" className={className} data-fs-product-card-tax-label=\"\">\n {children ?? taxesLabel}\n </Text>\n );\n}\n\nexport function ProductCardDiscount({\n className,\n children,\n format = 'percentage',\n ...props\n}: ProductCardDiscountProps) {\n const item = useProductListItem();\n const { usePriceWithTaxes } = useProductListConfig();\n const { locale, currency } = useStorefrontContext();\n\n const { listPrice, price } = item;\n if (!listPrice || listPrice <= price) {\n return null;\n }\n\n const amountOff = listPrice - price;\n const percentOff = Math.round((amountOff / listPrice) * 100);\n const tier = percentOff >= 30 ? 'high' : percentOff >= 15 ? 'medium' : 'low';\n // The percentage is tax-invariant (both prices scale by the same factor), but the absolute amount\n // must match the displayed prices, which are tax-inclusive when usePriceWithTaxes is enabled.\n const displayedAmountOff = usePriceWithTaxes ? priceWithTaxes(amountOff, item) : amountOff;\n const label =\n format === 'absolute'\n ? `-${formatPrice(displayedAmountOff, locale, item.currency ?? currency)}`\n : `-${percentOff}%`;\n\n return (\n <Badge {...props} className={className} data-fs-product-card-discount=\"\" data-fs-product-card-discount-tier={tier}>\n {children ?? label}\n </Badge>\n );\n}\n\nexport function ProductCardAddToCart({\n className,\n children,\n onClick,\n $actions: $actionsProp,\n ...props\n}: ProductCardAddToCartProps) {\n const $actions = useActionsOverride($actionsProp);\n const item = useProductListItem();\n\n function handleClick(e: React.MouseEvent<HTMLButtonElement>) {\n void $actions?.cart?.addItem?.({ variantId: item.id, seller: '1' });\n onClick?.(e);\n }\n\n return (\n <Button {...props} className={className} onClick={handleClick} data-fs-product-card-add-to-cart=\"\">\n {children}\n </Button>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Namespace export\n// ---------------------------------------------------------------------------\n\nexport const ProductList = {\n Root: ProductListRoot,\n Grid: ProductListGrid,\n Card: ProductCard,\n CardImage: ProductCardImage,\n CardName: ProductCardName,\n CardPrice: ProductCardPrice,\n CardListPrice: ProductCardListPrice,\n CardTaxLabel: ProductCardTaxLabel,\n CardDiscount: ProductCardDiscount,\n CardAddToCart: ProductCardAddToCart,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"product-list.js","names":[],"sources":["../../../src/matter/product-list/product-list.tsx"],"sourcesContent":["'use client';\n\nimport { defineComponent } from '@vtex/fs-core';\nimport { type ComponentPropsWithRef, createContext, useContext, useMemo } from 'react';\nimport { Badge } from '../../particles/badge/badge.js';\nimport { Button } from '../../particles/button/button.js';\nimport { Container } from '../../particles/container/container.js';\nimport { Image } from '../../particles/image/image.js';\nimport { Text } from '../../particles/text/text.js';\nimport { useActionsOverride } from '../../sdk/actions/actions-override-context.js';\nimport type { ComponentActions } from '../../sdk/actions/types.js';\nimport { useStorefrontContext } from '../../sdk/context/storefront-context.js';\n\n// ---------------------------------------------------------------------------\n// Product list config context (tax display)\n// ---------------------------------------------------------------------------\n\ntype ProductListConfig = {\n usePriceWithTaxes: boolean;\n taxesLabel: string;\n};\n\nexport const ProductListConfigContext = createContext<ProductListConfig>({\n usePriceWithTaxes: false,\n taxesLabel: 'Tax included',\n});\n\nfunction useProductListConfig(): ProductListConfig {\n return useContext(ProductListConfigContext);\n}\n\nfunction formatPrice(value: number, locale: string, currency: string): string {\n return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value);\n}\n\n// Tax rate for the item: the explicit `taxPercentage` (e.g. 0.18) wins; otherwise it is derived\n// from the absolute tax amount over the price. Missing/zero-price data yields 0 (no change).\nfunction taxRate(item: ProductListItem): number {\n if (item.taxPercentage != null) {\n return item.taxPercentage;\n }\n if (item.tax != null && item.price > 0) {\n return item.tax / item.price;\n }\n return 0;\n}\n\nfunction priceWithTaxes(base: number, item: ProductListItem): number {\n return base * (1 + taxRate(item));\n}\n\n// ---------------------------------------------------------------------------\n// Product list item types\n// ---------------------------------------------------------------------------\n\nexport type ProductListItemImage = {\n src: string;\n alt: string;\n};\n\nexport type ProductListItem = {\n id: string;\n name: string;\n image: ProductListItemImage;\n price: number;\n listPrice?: number;\n variant?: string;\n slug?: string;\n // PDP path segment in the active locale; build product links from this, not `slug`.\n routeSlug?: string;\n // Widened for the Product Shelf (T2): fields the card parts T3–T7 read straight off the\n // per-item context. Optional so the plain-`products` ProductListRoot (PLP) stays valid.\n spotPrice?: number;\n tax?: number;\n taxPercentage?: number | null;\n availableForSale?: boolean;\n availableQuantity?: number;\n currency?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Products context (provided by Root, consumed by Grid)\n// ---------------------------------------------------------------------------\n\nconst ProductListProductsContext = createContext<ProductListItem[]>([]);\n\n// ---------------------------------------------------------------------------\n// Item context (provided by Grid per product)\n// ---------------------------------------------------------------------------\n\nexport const ProductListItemContext = createContext<ProductListItem | null>(null);\n\nfunction useProductListItem(): ProductListItem {\n const item = useContext(ProductListItemContext);\n if (!item) {\n throw new Error('ProductCard components must be used inside ProductListGrid.');\n }\n return item;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type ProductListRootProps = ComponentPropsWithRef<'div'> & {\n products?: ProductListItem[];\n /** When true, card prices render tax-inclusive (from the item's tax data). Default false. */\n usePriceWithTaxes?: boolean;\n /** Text for the tax label part when tax display is on. Default `'Tax included'`. */\n taxesLabel?: string;\n};\n\nexport type ProductListGridProps = ComponentPropsWithRef<'div'>;\n\nexport type ProductCardProps = ComponentPropsWithRef<'div'> & {\n /** Toggles the card border. Reflects onto data-fs-product-card-bordered; a swatch draws the border. Default true. */\n bordered?: boolean;\n};\n\nexport type ProductCardImageProps = ComponentPropsWithRef<'img'> & {\n objectFit?: 'contain' | 'cover' | 'fill' | 'none';\n /** CSS `aspect-ratio` (e.g. `'1/1'`, `'3/4'`, `'16/9'`); lays out the image box, orthogonal to `objectFit`. */\n aspectRatio?: string;\n};\n\nexport type ProductCardNameProps = ComponentPropsWithRef<'span'>;\n\nexport type ProductCardPriceProps = ComponentPropsWithRef<'span'>;\n\nexport type ProductCardListPriceProps = ComponentPropsWithRef<'span'>;\n\nexport type ProductCardTaxLabelProps = ComponentPropsWithRef<'span'>;\n\nexport type ProductCardDiscountProps = ComponentPropsWithRef<'span'> & {\n format?: 'percentage' | 'absolute';\n};\n\nexport type ProductCardAddToCartProps = ComponentPropsWithRef<'button'> & {\n /** Injected by the compiler from `actions: { cart: ['addItem'] }`; the cart write surface. */\n $actions?: ComponentActions;\n};\n\n// ---------------------------------------------------------------------------\n// Definitions\n// ---------------------------------------------------------------------------\n\nexport const ProductListRootDefinition = defineComponent<ProductListRootProps>({\n name: 'ProductListRoot',\n importPath: '@vtex/faststore/components',\n component: ProductListRoot,\n editor: { label: 'Product List Root', category: 'commerce' },\n schema: {\n usePriceWithTaxes: { type: 'boolean', default: false, label: 'Use Price With Taxes' },\n taxesLabel: { type: 'string', default: 'Tax included', label: 'Taxes Label' },\n },\n guideline:\n 'Top-level wrapper for the product list; compose as ProductListRoot > ProductListGrid > ProductCard* children. Set usePriceWithTaxes to render tax-inclusive card prices and compose ProductCardTaxLabel to show the taxesLabel.',\n});\n\nexport const ProductListGridDefinition = defineComponent<ProductListGridProps>({\n name: 'ProductListGrid',\n importPath: '@vtex/faststore/components',\n component: ProductListGrid,\n editor: { label: 'Product List Grid', category: 'commerce' },\n schema: {},\n guideline:\n 'Repeating grid that renders the ProductCard* template for each product; must be a direct child of ProductListRoot.',\n});\n\nexport const ProductCardDefinition = defineComponent<ProductCardProps>({\n name: 'ProductCard',\n importPath: '@vtex/faststore/components',\n component: ProductCard,\n editor: { label: 'Product Card', category: 'commerce' },\n schema: {\n bordered: { type: 'boolean', default: true, label: 'Bordered' },\n },\n guideline:\n 'Optional card wrapper for the ProductCard* parts; always emits data-fs-product-card and data-fs-product-card-bordered (true/false, from the bordered toggle), and adds data-fs-product-card-out-of-stock when the product is unavailable (derived from availability, no prop). The border look is a swatch keyed off data-fs-product-card-bordered. Renders all children unchanged. Must be inside ProductListGrid / ProductShelfItems.',\n});\n\nexport const ProductCardImageDefinition = defineComponent<ProductCardImageProps>({\n name: 'ProductCardImage',\n importPath: '@vtex/faststore/components',\n component: ProductCardImage,\n editor: { label: 'Product Card Image', category: 'commerce' },\n schema: {\n objectFit: { type: ['contain', 'cover', 'fill', 'none'] as const, default: 'cover', label: 'Object Fit' },\n aspectRatio: { type: 'string', label: 'Aspect Ratio' },\n },\n guideline:\n 'Renders the product image with a configurable aspectRatio (e.g. 1/1, 3/4, 16/9) and objectFit. Alt text comes from the catalog. Must be inside ProductListGrid.',\n});\n\nexport const ProductCardNameDefinition = defineComponent<ProductCardNameProps>({\n name: 'ProductCardName',\n importPath: '@vtex/faststore/components',\n component: ProductCardName,\n editor: { label: 'Product Card Name', category: 'commerce' },\n schema: {},\n guideline: 'Renders the product name. Must be inside ProductListGrid.',\n});\n\nexport const ProductCardPriceDefinition = defineComponent<ProductCardPriceProps>({\n name: 'ProductCardPrice',\n importPath: '@vtex/faststore/components',\n component: ProductCardPrice,\n editor: { label: 'Product Card Price', category: 'commerce' },\n schema: {},\n guideline:\n 'Displays the selling price formatted from the active storefront context; renders tax-inclusive when usePriceWithTaxes is enabled. Must be inside ProductListGrid.',\n});\n\nexport const ProductCardListPriceDefinition = defineComponent<ProductCardListPriceProps>({\n name: 'ProductCardListPrice',\n importPath: '@vtex/faststore/components',\n component: ProductCardListPrice,\n editor: { label: 'Product Card List Price', category: 'commerce' },\n schema: {},\n guideline:\n 'Displays the original (strikethrough) list price when it exceeds the selling price; renders tax-inclusive when usePriceWithTaxes is enabled. Must be inside ProductListGrid.',\n});\n\nexport const ProductCardTaxLabelDefinition = defineComponent<ProductCardTaxLabelProps>({\n name: 'ProductCardTaxLabel',\n importPath: '@vtex/faststore/components',\n component: ProductCardTaxLabel,\n editor: { label: 'Product Card Tax Label', category: 'commerce' },\n schema: {},\n guideline:\n 'Tax label (e.g. \"Tax included\") shown beside the price. Renders only when the ProductListRoot/ProductShelfRoot config usePriceWithTaxes is enabled (state-driven, no toggle); the text comes from the taxesLabel config. Compose it to place the label, omit it to hide it. Must be inside ProductListGrid / ProductShelfItems.',\n});\n\nexport const ProductCardDiscountDefinition = defineComponent<ProductCardDiscountProps>({\n name: 'ProductCardDiscount',\n importPath: '@vtex/faststore/components',\n component: ProductCardDiscount,\n editor: { label: 'Product Card Discount', category: 'commerce' },\n schema: {\n format: { type: ['percentage', 'absolute'] as const, default: 'percentage', label: 'Discount Format' },\n },\n guideline:\n 'Discount badge for the current product; auto-computes the markdown from listPrice vs price and renders only when a discount exists. Compose it to show the badge, omit it to hide it (no toggle). Must be inside ProductListGrid / ProductShelfItems.',\n});\n\nexport const ProductCardAddToCartDefinition = defineComponent<ProductCardAddToCartProps>({\n name: 'ProductCardAddToCart',\n importPath: '@vtex/faststore/components',\n component: ProductCardAddToCart,\n editor: { label: 'Product Card Add to Cart', category: 'commerce' },\n schema: {},\n actions: { cart: ['addItem'] },\n guideline: 'Add-to-cart button for the current product card; must be inside ProductListGrid.',\n});\n\n// ---------------------------------------------------------------------------\n// Implementations\n// ---------------------------------------------------------------------------\n\nexport function ProductListRoot({\n usePriceWithTaxes = false,\n taxesLabel = 'Tax included',\n products = [],\n className,\n children,\n ...props\n}: ProductListRootProps) {\n const config = useMemo(() => ({ usePriceWithTaxes, taxesLabel }), [usePriceWithTaxes, taxesLabel]);\n\n return (\n <ProductListConfigContext value={config}>\n <ProductListProductsContext value={products}>\n <Container {...props} className={className} data-fs-product-list-root=\"\">\n {children}\n </Container>\n </ProductListProductsContext>\n </ProductListConfigContext>\n );\n}\n\nexport function ProductListGrid({ className, children, ...props }: ProductListGridProps) {\n const products = useContext(ProductListProductsContext);\n\n return (\n <Container {...props} className={className} data-fs-product-list-grid=\"\">\n {products.map((product) => (\n <ProductListItemContext key={product.id} value={product}>\n {children}\n </ProductListItemContext>\n ))}\n </Container>\n );\n}\n\nexport function ProductCard({ className, children, bordered = true, ...props }: ProductCardProps) {\n const item = useProductListItem();\n const outOfStock = item.availableForSale === false || item.availableQuantity === 0;\n\n return (\n <Container\n {...props}\n className={className}\n data-fs-product-card=\"\"\n data-fs-product-card-bordered={String(bordered)}\n data-fs-product-card-out-of-stock={outOfStock ? '' : undefined}\n >\n {children}\n </Container>\n );\n}\n\nexport function ProductCardImage({ className, objectFit = 'cover', aspectRatio, ...props }: ProductCardImageProps) {\n const item = useProductListItem();\n\n return (\n <Image\n {...props}\n src={item.image.src}\n alt={item.image.alt}\n aspectRatio={aspectRatio}\n className={className}\n style={{ objectFit, ...props.style }}\n data-fs-product-card-image=\"\"\n />\n );\n}\n\nexport function ProductCardName({ className, children, ...props }: ProductCardNameProps) {\n const item = useProductListItem();\n\n return (\n <Text {...props} as=\"span\" className={className} data-fs-product-card-name=\"\">\n {children ?? item.name}\n </Text>\n );\n}\n\nexport function ProductCardPrice({ className, children, ...props }: ProductCardPriceProps) {\n const item = useProductListItem();\n const { usePriceWithTaxes } = useProductListConfig();\n const { locale, currency } = useStorefrontContext();\n const value = usePriceWithTaxes ? priceWithTaxes(item.price, item) : item.price;\n\n return (\n <Text {...props} as=\"span\" className={className} data-fs-product-card-price=\"\">\n {children ?? formatPrice(value, locale, item.currency ?? currency)}\n </Text>\n );\n}\n\nexport function ProductCardListPrice({ className, children, ...props }: ProductCardListPriceProps) {\n const item = useProductListItem();\n const { usePriceWithTaxes } = useProductListConfig();\n const { locale, currency } = useStorefrontContext();\n\n if (!item.listPrice || item.listPrice <= item.price) {\n return null;\n }\n\n const value = usePriceWithTaxes ? priceWithTaxes(item.listPrice, item) : item.listPrice;\n\n return (\n <Text {...props} as=\"span\" className={className} data-fs-product-card-list-price=\"\">\n {children ?? formatPrice(value, locale, item.currency ?? currency)}\n </Text>\n );\n}\n\nexport function ProductCardTaxLabel({ className, children, ...props }: ProductCardTaxLabelProps) {\n const { usePriceWithTaxes, taxesLabel } = useProductListConfig();\n\n if (!usePriceWithTaxes) {\n return null;\n }\n\n return (\n <Text {...props} as=\"span\" className={className} data-fs-product-card-tax-label=\"\">\n {children ?? taxesLabel}\n </Text>\n );\n}\n\nexport function ProductCardDiscount({\n className,\n children,\n format = 'percentage',\n ...props\n}: ProductCardDiscountProps) {\n const item = useProductListItem();\n const { usePriceWithTaxes } = useProductListConfig();\n const { locale, currency } = useStorefrontContext();\n\n const { listPrice, price } = item;\n if (!listPrice || listPrice <= price) {\n return null;\n }\n\n const amountOff = listPrice - price;\n const percentOff = Math.round((amountOff / listPrice) * 100);\n const tier = percentOff >= 30 ? 'high' : percentOff >= 15 ? 'medium' : 'low';\n // The percentage is tax-invariant (both prices scale by the same factor), but the absolute amount\n // must match the displayed prices, which are tax-inclusive when usePriceWithTaxes is enabled.\n const displayedAmountOff = usePriceWithTaxes ? priceWithTaxes(amountOff, item) : amountOff;\n const label =\n format === 'absolute'\n ? `-${formatPrice(displayedAmountOff, locale, item.currency ?? currency)}`\n : `-${percentOff}%`;\n\n return (\n <Badge {...props} className={className} data-fs-product-card-discount=\"\" data-fs-product-card-discount-tier={tier}>\n {children ?? label}\n </Badge>\n );\n}\n\nexport function ProductCardAddToCart({\n className,\n children,\n onClick,\n $actions: $actionsProp,\n ...props\n}: ProductCardAddToCartProps) {\n const $actions = useActionsOverride($actionsProp);\n const item = useProductListItem();\n\n function handleClick(e: React.MouseEvent<HTMLButtonElement>) {\n void $actions?.cart?.addItem?.({ variantId: item.id, seller: '1' });\n onClick?.(e);\n }\n\n return (\n <Button {...props} className={className} onClick={handleClick} data-fs-product-card-add-to-cart=\"\">\n {children}\n </Button>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Namespace export\n// ---------------------------------------------------------------------------\n\nexport const ProductList = {\n Root: ProductListRoot,\n Grid: ProductListGrid,\n Card: ProductCard,\n CardImage: ProductCardImage,\n CardName: ProductCardName,\n CardPrice: ProductCardPrice,\n CardListPrice: ProductCardListPrice,\n CardTaxLabel: ProductCardTaxLabel,\n CardDiscount: ProductCardDiscount,\n CardAddToCart: ProductCardAddToCart,\n};\n"],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@ const fragment = `
|
|
|
16
16
|
id
|
|
17
17
|
title
|
|
18
18
|
slug
|
|
19
|
+
routeSlug
|
|
19
20
|
images(first: 1) {
|
|
20
21
|
edges {
|
|
21
22
|
node {
|
|
@@ -149,6 +150,7 @@ function transformProducts(data) {
|
|
|
149
150
|
id: node.id,
|
|
150
151
|
name: node.title,
|
|
151
152
|
slug: node.slug,
|
|
153
|
+
routeSlug: node.routeSlug,
|
|
152
154
|
image: {
|
|
153
155
|
src: image?.url ?? "",
|
|
154
156
|
alt: image?.altText ?? node.title
|