@faststore/core 4.8.1-dev.0 → 4.8.1-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.turbo/turbo-generate.log +3 -3
  2. package/.turbo/turbo-test.log +152 -114
  3. package/CHANGELOG.md +10 -0
  4. package/cms/faststore/my-account/components/cms_component__accountnavigation.jsonc +10 -0
  5. package/cms/faststore/my-account/components/cms_component__accountorderdelivery.jsonc +80 -0
  6. package/cms/faststore/my-account/components/cms_component__accountorderdetails.jsonc +25 -0
  7. package/cms/faststore/my-account/components/cms_component__accountorderslist.jsonc +10 -0
  8. package/cms/faststore/my-account/components/cms_component__accountorderstatus.jsonc +90 -0
  9. package/cms/faststore/my-account/components/cms_component__accountordersummary.jsonc +25 -0
  10. package/package.json +6 -6
  11. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerFooter.tsx +13 -2
  12. package/src/components/account/orders/ListOrders/FilterSlider/FilterFacetPendingApproval/FilterFacetPendingApproval.tsx +8 -3
  13. package/src/components/account/orders/ListOrders/FilterSlider/FilterSlider.tsx +1 -0
  14. package/src/components/account/orders/ListOrders/ListOrders.tsx +2 -2
  15. package/src/components/account/orders/ListOrders/listOrdersLabels.ts +4 -0
  16. package/src/components/account/orders/OrderDetails/BuyingPolicyAlert/BuyingPolicyAlert.tsx +45 -9
  17. package/src/components/account/orders/OrderDetails/DeliveryCard/DeliveryCard.tsx +14 -3
  18. package/src/components/account/orders/OrderDetails/DeliveryOptionAccordion/DeliveryOptionAccordion.tsx +4 -1
  19. package/src/components/account/orders/OrderDetails/DeliveryOptionAccordion/DeliveryOptionAccordionDeliveryInfo.tsx +14 -7
  20. package/src/components/account/orders/OrderDetails/DeliveryOptionAccordion/DeliveryOptionAccordionProducts.tsx +8 -5
  21. package/src/components/account/orders/OrderDetails/OrderDetailsHeader.tsx +5 -0
  22. package/src/components/account/orders/OrderDetails/StatusCard/StatusCard.tsx +26 -89
  23. package/src/components/account/orders/OrderDetails/StatusCard/statusCardLocalization.ts +114 -0
  24. package/src/components/account/orders/OrderDetails/SummaryCard/SummaryCard.tsx +2 -1
  25. package/src/components/account/orders/OrderDetails/SummaryCard/getOrderTotalLabel.ts +19 -0
  26. package/src/components/account/orders/OrderDetails/getDeliveryOptionLabel.ts +135 -0
  27. package/src/components/account/orders/OrderDetails/orderDetailsLabels.ts +73 -0
  28. package/src/components/sections/Account/AccountOrderDelivery.tsx +3 -1
  29. package/src/components/sections/Account/AccountOrderDetails.tsx +16 -0
  30. package/src/components/sections/Account/AccountOrderStatus.tsx +27 -3
  31. package/src/components/sections/Account/AccountOrdersList.tsx +4 -0
  32. package/src/pages/api/graphql.ts +108 -69
  33. package/src/sdk/account/accountPageContext.ts +14 -3
  34. package/src/sdk/account/getMyAccountRoutes.ts +2 -0
  35. package/src/sdk/overrides/PageProvider.tsx +1 -1
  36. package/test/components/account/OrganizationDrawerFooter.browser.test.tsx +95 -1
  37. package/test/components/account/orders/BuyingPolicyAlert.browser.test.tsx +342 -0
  38. package/test/components/account/orders/DeliveryCard.browser.test.tsx +36 -0
  39. package/test/components/account/orders/DeliveryOptionAccordion.browser.test.tsx +76 -0
  40. package/test/components/account/orders/DeliveryOptionAccordionDeliveryInfo.browser.test.tsx +41 -0
  41. package/test/components/account/orders/DeliveryOptionAccordionProducts.browser.test.tsx +35 -0
  42. package/test/components/account/orders/FilterFacetPendingApproval.browser.test.tsx +25 -0
  43. package/test/components/account/orders/StatusCard.browser.test.tsx +37 -0
  44. package/test/components/account/orders/StatusCard.test.ts +101 -0
  45. package/test/components/account/orders/SummaryCard.browser.test.tsx +59 -0
  46. package/test/components/account/orders/getDeliveryOptionLabel.test.ts +129 -0
  47. package/test/components/account/orders/getOrderTotalLabel.test.ts +35 -0
  48. package/test/components/account/orders/listOrdersLabels.test.ts +23 -0
  49. package/test/components/account/orders/orderDetailsLabels.test.ts +13 -0
  50. package/test/components/sections/Account/AccountOrderDelivery.browser.test.tsx +72 -0
  51. package/test/components/sections/Account/AccountOrderDetails.browser.test.tsx +151 -0
  52. package/test/components/sections/Account/AccountOrderStatus.browser.test.tsx +59 -0
  53. package/test/components/sections/Account/AccountOrdersList.browser.test.tsx +67 -0
  54. package/test/pages/api/graphql.test.ts +27 -1
  55. package/test/sdk/account/getMyAccountRoutes.test.ts +11 -1
  56. package/test/utils/userOrderStatus.test.ts +22 -0
@@ -7,20 +7,34 @@ import {
7
7
  type AccountOrderDetailsPageData,
8
8
  useAccountPageData,
9
9
  } from 'src/sdk/account/accountPageContext'
10
- import type { OrderStatusKey } from 'src/utils/userOrderStatus'
10
+ import type {
11
+ OrderStatusCmsLabels,
12
+ OrderStatusKey,
13
+ } from 'src/utils/userOrderStatus'
11
14
  import Section from '../Section'
12
15
 
13
- export type AccountOrderStatusProps = OrderStatusSectionLabels
16
+ export type AccountOrderStatusProps = OrderStatusSectionLabels &
17
+ OrderStatusCmsLabels
14
18
 
15
19
  const AccountOrderStatus = ({
16
20
  statusTitle = defaultOrderStatusLabels.statusTitle,
17
21
  orderPlacedStep = defaultOrderStatusLabels.orderPlacedStep,
22
+ approvedStep = defaultOrderStatusLabels.approvedStep,
23
+ pendingApprovalStep = defaultOrderStatusLabels.pendingApprovalStep,
24
+ deniedStep = defaultOrderStatusLabels.deniedStep,
25
+ paymentApprovedStep = defaultOrderStatusLabels.paymentApprovedStep,
18
26
  paymentPendingStep = defaultOrderStatusLabels.paymentPendingStep,
27
+ paymentAuthorizationStep = defaultOrderStatusLabels.paymentAuthorizationStep,
28
+ paymentDeniedStep = defaultOrderStatusLabels.paymentDeniedStep,
29
+ readyForDeliveryStep = defaultOrderStatusLabels.readyForDeliveryStep,
19
30
  handlingStep = defaultOrderStatusLabels.handlingStep,
31
+ canceledStep = defaultOrderStatusLabels.canceledStep,
20
32
  invoicedStep = defaultOrderStatusLabels.invoicedStep,
21
33
  deliveredStep = defaultOrderStatusLabels.deliveredStep,
34
+ shipOrderStep = defaultOrderStatusLabels.shipOrderStep,
22
35
  }: AccountOrderStatusProps) => {
23
- const { order } = useAccountPageData<AccountOrderDetailsPageData>()
36
+ const { order, orderStatusLabels } =
37
+ useAccountPageData<AccountOrderDetailsPageData>()
24
38
 
25
39
  if (!order) {
26
40
  return null
@@ -31,13 +45,23 @@ const AccountOrderStatus = ({
31
45
  <StatusCard
32
46
  status={order.status as OrderStatusKey}
33
47
  creationDate={order.creationDate}
48
+ statusLabels={orderStatusLabels}
34
49
  labels={{
35
50
  statusTitle,
36
51
  orderPlacedStep,
52
+ approvedStep,
53
+ pendingApprovalStep,
54
+ deniedStep,
55
+ paymentApprovedStep,
37
56
  paymentPendingStep,
57
+ paymentAuthorizationStep,
58
+ paymentDeniedStep,
59
+ readyForDeliveryStep,
38
60
  handlingStep,
61
+ canceledStep,
39
62
  invoicedStep,
40
63
  deliveredStep,
64
+ shipOrderStep,
41
65
  }}
42
66
  />
43
67
  </Section>
@@ -21,6 +21,8 @@ const AccountOrdersList = ({
21
21
  placedOnLabel = defaultListOrdersLabels.placedOnLabel,
22
22
  deliveryByLabel = defaultListOrdersLabels.deliveryByLabel,
23
23
  pendingApprovalLabel = defaultListOrdersLabels.pendingApprovalLabel,
24
+ statusFilterLabel = defaultListOrdersLabels.statusFilterLabel,
25
+ orderDateFilterLabel = defaultListOrdersLabels.orderDateFilterLabel,
24
26
  paginationOfLabel = defaultListOrdersLabels.paginationOfLabel,
25
27
  previousPageLabel = defaultListOrdersLabels.previousPageLabel,
26
28
  nextPageLabel = defaultListOrdersLabels.nextPageLabel,
@@ -61,6 +63,8 @@ const AccountOrdersList = ({
61
63
  placedOnLabel,
62
64
  deliveryByLabel,
63
65
  pendingApprovalLabel,
66
+ statusFilterLabel,
67
+ orderDateFilterLabel,
64
68
  paginationOfLabel,
65
69
  previousPageLabel,
66
70
  nextPageLabel,
@@ -150,6 +150,112 @@ const hasVtexIdclientAutCookie = (request: NextApiRequest): boolean => {
150
150
  )
151
151
  }
152
152
 
153
+ /**
154
+ * Responds to a GraphQL execution that returned errors: recovers the
155
+ * upstream status from a FastStoreError when possible instead of collapsing
156
+ * everything to 500, and logs every error for observability.
157
+ */
158
+ const respondWithGraphqlErrors = (
159
+ errors: readonly unknown[],
160
+ response: Parameters<NextApiHandler>[1]
161
+ ) => {
162
+ // After error masking, entries are GraphQLError instances whose `name` is
163
+ // "GraphQLError" — the original FastStoreError (carrying the upstream
164
+ // status) is nested in `originalError`. Recover it so the BFF propagates
165
+ // the real status instead of collapsing everything to 500.
166
+ const fastStoreError = errors.map(recoverFastStoreError).find(Boolean)
167
+ const reportedErrors = errors.map((graphqlError) => {
168
+ const fsError = recoverFastStoreError(graphqlError)
169
+
170
+ return {
171
+ message: (graphqlError as { message?: string })?.message,
172
+ status: fsError?.extensions.status,
173
+ type: fsError?.extensions.type,
174
+ }
175
+ })
176
+
177
+ console.error('Graphql execution returned with error: ', reportedErrors)
178
+ OTELLogger(
179
+ 'error',
180
+ 'Graphql execution returned with error: %o',
181
+ reportedErrors
182
+ )
183
+
184
+ const status = fastStoreError?.extensions.status ?? 500
185
+
186
+ // Error responses are never cacheable: some upstream statuses (404, 410,
187
+ // ...) are heuristically cacheable by intermediaries per RFC 9111 §4.2.2,
188
+ // which would let a CDN cache an error for this operation.
189
+ response.setHeader('cache-control', 'no-store')
190
+
191
+ // No recoverable FastStoreError: keep the masked, body-less 500.
192
+ if (!fastStoreError) {
193
+ response.status(status).end()
194
+ return
195
+ }
196
+
197
+ // Only FastStoreError-derived details are exposed. `type`/`status` are a
198
+ // closed enum (safe everywhere); the free-text `message` may echo raw
199
+ // upstream text, so it is restricted to non-production responses. The
200
+ // full message is still available server-side via the log above.
201
+ const responseError = {
202
+ extensions: {
203
+ type: fastStoreError.extensions.type,
204
+ status: fastStoreError.extensions.status,
205
+ },
206
+ ...(process.env.NODE_ENV === 'production'
207
+ ? {}
208
+ : { message: fastStoreError.message }),
209
+ }
210
+
211
+ response.status(status)
212
+ response.setHeader('content-type', 'application/json')
213
+ response.send(JSON.stringify({ errors: [responseError] }))
214
+ }
215
+
216
+ /**
217
+ * Responds to an error thrown out of the main handler try-block (a rejected
218
+ * `parseRequest`/`execute`, or anything else unexpected).
219
+ */
220
+ const respondToUnexpectedError = (
221
+ err: unknown,
222
+ response: Parameters<NextApiHandler>[1]
223
+ ) => {
224
+ // Same rationale as `respondWithGraphqlErrors`: a 400/401/500 here must
225
+ // never be cached. This path can run after the success branch has already
226
+ // set a cacheable cache-control (e.g. `setHeader('set-cookie', ...)`
227
+ // rejecting a malformed upstream cookie, or `JSON.stringify` throwing
228
+ // after cache-control was set but before `send` completed). Guarded by
229
+ // `headersSent`: if the throw came from `send()` itself after headers were
230
+ // already flushed, `setHeader` would throw ERR_HTTP_HEADERS_SENT and this
231
+ // would fail to produce a handled response.
232
+ if (!response.headersSent) {
233
+ response.setHeader('cache-control', 'no-store')
234
+ }
235
+
236
+ console.error(
237
+ 'Something unexpected occurred querying Graphql endpoint: \n',
238
+ err
239
+ )
240
+ OTELLogger(
241
+ 'error',
242
+ 'Something unexpected occurred querying Graphql endpoint: %o',
243
+ err
244
+ )
245
+
246
+ if (err instanceof BadRequestError) {
247
+ response.status(400).end()
248
+ return
249
+ }
250
+
251
+ if (err instanceof UnauthorizedError) {
252
+ response.status(401).end()
253
+ return
254
+ }
255
+
256
+ response.status(500).end()
257
+ }
258
+
153
259
  const handler: NextApiHandler = async (request, response) => {
154
260
  if (request.method !== 'POST' && request.method !== 'GET') {
155
261
  response.status(405).end()
@@ -222,53 +328,7 @@ const handler: NextApiHandler = async (request, response) => {
222
328
  const hasErrors = Array.isArray(errors) && errors.length > 0
223
329
 
224
330
  if (hasErrors) {
225
- // After error masking, entries are GraphQLError instances whose
226
- // `name` is "GraphQLError" — the original FastStoreError (carrying the
227
- // upstream status) is nested in `originalError`. Recover it so the BFF
228
- // propagates the real status instead of collapsing everything to 500.
229
- const fastStoreError = errors.map(recoverFastStoreError).find(Boolean)
230
- const reportedErrors = errors.map((graphqlError) => {
231
- const fsError = recoverFastStoreError(graphqlError)
232
-
233
- return {
234
- message: (graphqlError as { message?: string })?.message,
235
- status: fsError?.extensions.status,
236
- type: fsError?.extensions.type,
237
- }
238
- })
239
-
240
- console.error('Graphql execution returned with error: ', reportedErrors)
241
- OTELLogger(
242
- 'error',
243
- 'Graphql execution returned with error: %o',
244
- reportedErrors
245
- )
246
-
247
- const status = fastStoreError?.extensions.status ?? 500
248
-
249
- // No recoverable FastStoreError: keep the masked, body-less 500.
250
- if (!fastStoreError) {
251
- response.status(status).end()
252
- return
253
- }
254
-
255
- // Only FastStoreError-derived details are exposed. `type`/`status` are a
256
- // closed enum (safe everywhere); the free-text `message` may echo raw
257
- // upstream text, so it is restricted to non-production responses. The
258
- // full message is still available server-side via the log above.
259
- const responseError = {
260
- extensions: {
261
- type: fastStoreError.extensions.type,
262
- status: fastStoreError.extensions.status,
263
- },
264
- ...(process.env.NODE_ENV !== 'production'
265
- ? { message: fastStoreError.message }
266
- : {}),
267
- }
268
-
269
- response.status(status)
270
- response.setHeader('content-type', 'application/json')
271
- response.send(JSON.stringify({ errors: [responseError] }))
331
+ respondWithGraphqlErrors(errors, response)
272
332
  return
273
333
  }
274
334
 
@@ -321,28 +381,7 @@ const handler: NextApiHandler = async (request, response) => {
321
381
  response.setHeader('content-type', 'application/json')
322
382
  response.send(JSON.stringify({ data, errors }))
323
383
  } catch (err) {
324
- console.error(
325
- 'Something unexpected occurred querying Graphql endpoint: \n',
326
- err
327
- )
328
- OTELLogger(
329
- 'error',
330
- 'Something unexpected occurred querying Graphql endpoint: %o',
331
- err
332
- )
333
-
334
- if (err instanceof BadRequestError) {
335
- response.status(400).end()
336
- return
337
- }
338
-
339
- if (err instanceof UnauthorizedError) {
340
- response.status(401).end()
341
- return
342
- }
343
-
344
- response.status(500).end()
345
- return
384
+ respondToUnexpectedError(err, response)
346
385
  }
347
386
  }
348
387
 
@@ -4,8 +4,9 @@ import type {
4
4
  ServerOrderDetailsQueryQuery,
5
5
  ServerProfileQueryQuery,
6
6
  } from '@generated/graphql'
7
+ import { useContext } from 'react'
7
8
  import type { PageGlobalContext } from 'src/sdk/overrides/PageProvider'
8
- import { usePage } from 'src/sdk/overrides/PageProvider'
9
+ import { PageContext, usePage } from 'src/sdk/overrides/PageProvider'
9
10
  import type { OrderStatusCmsLabels } from 'src/utils/userOrderStatus'
10
11
  import type { AccountNavigationLabels } from './getMyAccountRoutes'
11
12
 
@@ -76,9 +77,19 @@ export interface AccountPageContext extends PageGlobalContext {
76
77
  navigationLabels?: AccountNavigationLabels
77
78
  }
78
79
 
80
+ /**
81
+ * Same data as `usePage<AccountPageContext>().navigationLabels`, but safe to
82
+ * call from components (e.g. the B2B organization drawer) that render both
83
+ * inside and outside a My Account `PageProvider` tree — returns `undefined`
84
+ * instead of throwing when there is no provider, so callers fall back to
85
+ * their hardcoded default label.
86
+ */
79
87
  export function useAccountNavigationLabels() {
80
- const ctx = usePage<AccountPageContext>()
81
- return ctx.navigationLabels
88
+ const context = useContext(PageContext)?.context
89
+
90
+ return context && 'navigationLabels' in context
91
+ ? context.navigationLabels
92
+ : undefined
82
93
  }
83
94
 
84
95
  export function useAccountPageData<
@@ -22,6 +22,7 @@ export type AccountNavigationLabels = Partial<{
22
22
  companyLabel: string
23
23
  contractLabel: string
24
24
  cardsLabel: string
25
+ quotesLabel: string
25
26
  }>
26
27
 
27
28
  interface GetMyAccountRouteParams {
@@ -45,6 +46,7 @@ const ROUTE_LABEL_KEYS: Record<string, keyof AccountNavigationLabels> = {
45
46
  [USER_DETAILS_ROUTE]: 'userDetailsLabel',
46
47
  [SECURITY_ROUTE]: 'securityLabel',
47
48
  [CARDS_ROUTE]: 'cardsLabel',
49
+ [QUOTES_ROUTE]: 'quotesLabel',
48
50
  }
49
51
 
50
52
  // This is the default route list for My Account, we should add then as the feature is implemented
@@ -62,7 +62,7 @@ export const isSearchPage = (x: any): x is SearchPageContext =>
62
62
  x?.data?.title != undefined ||
63
63
  x?.data?.searchTerm != undefined
64
64
 
65
- const PageContext = createContext<PageProviderContextValue | null>(null)
65
+ export const PageContext = createContext<PageProviderContextValue | null>(null)
66
66
 
67
67
  function PageProvider({
68
68
  context,
@@ -6,6 +6,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
6
6
  import { describe, expect, it, vi } from 'vitest'
7
7
 
8
8
  import { OrganizationDrawerFooter } from '../../../src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerFooter'
9
+ import PageProvider from '../../../src/sdk/overrides/PageProvider'
9
10
 
10
11
  describe('OrganizationDrawerFooter', () => {
11
12
  it('renders org and user details with optional email', () => {
@@ -48,7 +49,100 @@ describe('OrganizationDrawerFooter', () => {
48
49
  />
49
50
  )
50
51
 
51
- fireEvent.click(screen.getByRole('button', { name: /log out/i }))
52
+ fireEvent.click(screen.getByRole('button', { name: /logout/i }))
52
53
  expect(onLogoutClick).toHaveBeenCalledTimes(1)
53
54
  })
55
+
56
+ it('uses the default English manage/logout labels when rendered outside a My Account page context', () => {
57
+ render(
58
+ <OrganizationDrawerFooter
59
+ orgName="Stellar Global"
60
+ userName="Jane Buyer"
61
+ showManageLink
62
+ manageUrl="/pvt/organization-account/org-unit/unit-1"
63
+ />
64
+ )
65
+
66
+ expect(screen.getByText('Manage')).toBeTruthy()
67
+ expect(screen.getByRole('button', { name: 'Logout' })).toBeTruthy()
68
+ })
69
+
70
+ it('uses the default labels inside a non-account page context', () => {
71
+ render(
72
+ <PageProvider context={{ globalSettings: {} }}>
73
+ <OrganizationDrawerFooter
74
+ orgName="Stellar Global"
75
+ userName="Jane Buyer"
76
+ showManageLink
77
+ manageUrl="/pvt/organization-account/org-unit/unit-1"
78
+ />
79
+ </PageProvider>
80
+ )
81
+
82
+ expect(screen.getByText('Manage')).toBeTruthy()
83
+ expect(screen.getByRole('button', { name: 'Logout' })).toBeTruthy()
84
+ })
85
+
86
+ it('uses the CMS-provided navigation labels when rendered inside a My Account page', () => {
87
+ render(
88
+ <PageProvider
89
+ context={{
90
+ accountPageData: {},
91
+ navigationLabels: {
92
+ manageLabel: 'Gerenciar',
93
+ logoutLabel: 'Sair',
94
+ },
95
+ }}
96
+ >
97
+ <OrganizationDrawerFooter
98
+ orgName="Stellar Global"
99
+ userName="Jane Buyer"
100
+ showManageLink
101
+ manageUrl="/pvt/organization-account/org-unit/unit-1"
102
+ />
103
+ </PageProvider>
104
+ )
105
+
106
+ expect(screen.getByText('Gerenciar')).toBeTruthy()
107
+ expect(screen.getByRole('button', { name: 'Sair' })).toBeTruthy()
108
+ })
109
+
110
+ it('lets an explicit label prop win over the CMS-provided navigation labels', () => {
111
+ render(
112
+ <PageProvider
113
+ context={{
114
+ accountPageData: {},
115
+ navigationLabels: {
116
+ manageLabel: 'Gerenciar',
117
+ logoutLabel: 'Sair',
118
+ },
119
+ }}
120
+ >
121
+ <OrganizationDrawerFooter
122
+ orgName="Stellar Global"
123
+ userName="Jane Buyer"
124
+ showManageLink
125
+ manageUrl="/pvt/organization-account/org-unit/unit-1"
126
+ manageLabel="Manage account"
127
+ logoutLabel="Sign out"
128
+ />
129
+ </PageProvider>
130
+ )
131
+
132
+ expect(screen.getByText('Manage account')).toBeTruthy()
133
+ expect(screen.getByRole('button', { name: 'Sign out' })).toBeTruthy()
134
+ })
135
+
136
+ it('does not render the manage link when showManageLink is false', () => {
137
+ render(
138
+ <OrganizationDrawerFooter
139
+ orgName="Stellar Global"
140
+ userName="Jane Buyer"
141
+ showManageLink={false}
142
+ manageUrl="/pvt/organization-account/org-unit/unit-1"
143
+ />
144
+ )
145
+
146
+ expect(screen.queryByText('Manage')).toBeNull()
147
+ })
54
148
  })