@faststore/core 3.99.0-dev.1 → 3.99.0-dev.4

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 (54) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +38 -38
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/config.json +3 -3
  5. package/.next/cache/webpack/client-production/0.pack +0 -0
  6. package/.next/cache/webpack/client-production/index.pack +0 -0
  7. package/.next/cache/webpack/server-production/0.pack +0 -0
  8. package/.next/cache/webpack/server-production/index.pack +0 -0
  9. package/.next/prerender-manifest.js +1 -1
  10. package/.next/prerender-manifest.json +1 -1
  11. package/.next/routes-manifest.json +1 -1
  12. package/.next/server/chunks/1741.js +2 -2
  13. package/.next/server/chunks/5133.js +1 -1
  14. package/.next/server/chunks/5212.js +1 -1
  15. package/.next/server/chunks/7121.js +1 -1
  16. package/.next/server/chunks/8971.js +5 -4
  17. package/.next/server/functions-config-manifest.json +1 -1
  18. package/.next/server/middleware-build-manifest.js +1 -1
  19. package/.next/server/pages/api/graphql.js +3 -3
  20. package/.next/server/pages/en-US/404.html +2 -2
  21. package/.next/server/pages/en-US/500.html +2 -2
  22. package/.next/server/pages/en-US/checkout.html +2 -2
  23. package/.next/server/pages/en-US/login.html +2 -2
  24. package/.next/server/pages/en-US/s.html +2 -2
  25. package/.next/server/pages/en-US.html +2 -2
  26. package/.next/server/pages/pvt/account/403.js +1 -1
  27. package/.next/server/pages-manifest.json +1 -1
  28. package/.next/static/chunks/2221-3b8af0bc108994c0.js +2 -0
  29. package/.next/static/chunks/pages/_app-87b58cddac61876b.js +1 -0
  30. package/.next/static/chunks/pages/pvt/account/{403-d44b70591d2c86dc.js → 403-f3cb4fbbbb801a00.js} +1 -1
  31. package/.next/static/chunks/{webpack-fd706604cbbdc60f.js → webpack-da79ff7bed51ca28.js} +1 -1
  32. package/.next/static/{HfM_skwMOUpIXO3y1WakQ → ezu8OJFco4zOT-ZPSNxeP}/_buildManifest.js +1 -1
  33. package/.next/trace +143 -142
  34. package/.turbo/turbo-build.log +16 -16
  35. package/.turbo/turbo-test.log +10 -9
  36. package/@generated/gql.ts +2 -2
  37. package/@generated/graphql.ts +6 -1
  38. package/@generated/persisted-documents.json +1 -1
  39. package/@generated/schema.graphql +2 -0
  40. package/CHANGELOG.md +17 -0
  41. package/discovery.config.default.js +1 -0
  42. package/package.json +7 -7
  43. package/src/components/account/MyAccountDrawer/OrganizationDrawer/useReloadAfterLogoutReturn.ts +6 -1
  44. package/src/pages/api/graphql.ts +6 -22
  45. package/src/sdk/account/useRefreshToken.ts +3 -14
  46. package/src/sdk/cart/index.ts +12 -2
  47. package/src/sdk/session/index.ts +96 -58
  48. package/src/sdk/session/storageKeys.ts +2 -0
  49. package/src/sdk/useStore.ts +7 -1
  50. package/src/utils/validateSessionRefreshToken.ts +29 -0
  51. package/test/utils/validateSessionRefreshToken.test.ts +69 -0
  52. package/.next/static/chunks/2221-b085ff74b21a2c2c.js +0 -2
  53. package/.next/static/chunks/pages/_app-8be996b3105d8c26.js +0 -1
  54. /package/.next/static/{HfM_skwMOUpIXO3y1WakQ → ezu8OJFco4zOT-ZPSNxeP}/_ssgManifest.js +0 -0
@@ -1,23 +1,23 @@
1
1
 
2
- > @faststore/core@3.99.0-dev.0 prebuild /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@3.99.0-dev.3 prebuild /home/runner/work/faststore/faststore/packages/core
3
3
  > na run partytown && na run generate
4
4
 
5
5
 
6
- > @faststore/core@3.99.0-dev.0 partytown /home/runner/work/faststore/faststore/packages/core
6
+ > @faststore/core@3.99.0-dev.3 partytown /home/runner/work/faststore/faststore/packages/core
7
7
  > partytown copylib ./public/~partytown
8
8
 
9
9
  Partytown lib copied to: /home/runner/work/faststore/faststore/packages/core/public/~partytown
10
10
 
11
- > @faststore/core@3.99.0-dev.0 generate /home/runner/work/faststore/faststore/packages/core
11
+ > @faststore/core@3.99.0-dev.3 generate /home/runner/work/faststore/faststore/packages/core
12
12
  > na run generate:schema && na run generate:codegen && na run format:generated
13
13
 
14
14
 
15
- > @faststore/core@3.99.0-dev.0 generate:schema /home/runner/work/faststore/faststore/packages/core
15
+ > @faststore/core@3.99.0-dev.3 generate:schema /home/runner/work/faststore/faststore/packages/core
16
16
  > tsx src/server/generator/generateGraphQLSchemaFile.ts
17
17
 
18
18
  Schema GraphQL file generated successfully
19
19
 
20
- > @faststore/core@3.99.0-dev.0 generate:codegen /home/runner/work/faststore/faststore/packages/core
20
+ > @faststore/core@3.99.0-dev.3 generate:codegen /home/runner/work/faststore/faststore/packages/core
21
21
  > graphql-codegen
22
22
 
23
23
  [STARTED] Parse Configuration
@@ -37,11 +37,11 @@ Running lifecycle hook "afterStart" scripts...
37
37
  [CLI] Loading Documents
38
38
  [CLI] Generating output
39
39
 
40
- > @faststore/core@3.99.0-dev.0 format:generated /home/runner/work/faststore/faststore/packages/core
40
+ > @faststore/core@3.99.0-dev.3 format:generated /home/runner/work/faststore/faststore/packages/core
41
41
  > prettier --write "@generated/**/*.{ts,js,tsx,jsx,json}" --loglevel error
42
42
 
43
43
 
44
- > @faststore/core@3.99.0-dev.0 build /home/runner/work/faststore/faststore/packages/core
44
+ > @faststore/core@3.99.0-dev.3 build /home/runner/work/faststore/faststore/packages/core
45
45
  > next build
46
46
 
47
47
  ⚠ No build cache found. Please configure build caching for faster rebuilds. Read more: https://nextjs.org/docs/messages/no-cache
@@ -59,39 +59,39 @@ https://nextjs.org/telemetry
59
59
  Collecting page data ...
60
60
  Generating static pages (0/6) ...
61
61
 
62
62
  Generating static pages (1/6)
63
- Warning: Dynamic Content not found for the page: home. Refer to the Dynamic Content documentation at https://developers.vtex.com/docs/guides/faststore/dynamic-content-overview for mapping the page and the corresponding data-fetching function.
64
63
 
65
64
  Generating static pages (2/6)
65
+ Warning: Dynamic Content not found for the page: home. Refer to the Dynamic Content documentation at https://developers.vtex.com/docs/guides/faststore/dynamic-content-overview for mapping the page and the corresponding data-fetching function.
66
66
 
67
67
  Generating static pages (4/6)
68
68
 
69
69
  ✓ Generating static pages (6/6)
70
70
  Finalizing page optimization ...
71
71
  Collecting build traces ...
72
72
 
73
73
  Route (pages) Size First Load JS
74
- ┌ ● / 7.39 kB 165 kB
74
+ ┌ ● / 7.39 kB 166 kB
75
75
  ├ └ css/02259c549b2179f2.css 3.1 kB
76
76
  ├ /_app 0 B 109 kB
77
77
  ├ ● /[...slug] 2.55 kB 175 kB
78
78
  ├ ● /[slug]/p 97.3 kB 255 kB
79
79
  ├ ├ css/e68c313e4ce2fd1b.css 6.34 kB
80
80
  ├ └ css/9b4c21dcd3bc4a17.css 16.7 kB
81
- ├ ○ /404 1.58 kB 159 kB
82
- ├ ● /500 1.57 kB 159 kB
81
+ ├ ○ /404 1.58 kB 160 kB
82
+ ├ ● /500 1.57 kB 160 kB
83
83
  ├ λ /api/fs/logout 0 B 109 kB
84
84
  ├ λ /api/graphql 0 B 109 kB
85
85
  ├ λ /api/health/live 0 B 109 kB
86
86
  ├ λ /api/health/ready 0 B 109 kB
87
87
  ├ λ /api/preview 0 B 109 kB
88
88
  ├ ● /checkout 745 B 159 kB
89
- ├ ● /login 1.7 kB 159 kB
89
+ ├ ● /login 1.7 kB 160 kB
90
90
  ├ λ /pvt/account 245 B 109 kB
91
91
  ├ ● /pvt/account/[...unknown] 286 B 109 kB
92
- ├ λ /pvt/account/403 2.99 kB 161 kB
92
+ ├ λ /pvt/account/403 2.98 kB 161 kB
93
93
  ├ └ css/094ad5cc00c74e64.css 4.82 kB
94
94
  ├ λ /pvt/account/404 2.18 kB 160 kB
95
95
  ├ └ css/72f154a00f1cf621.css 4.88 kB
96
96
  ├ λ /pvt/account/orders 9.89 kB 168 kB
97
97
  ├ └ css/4b2af1aaeb961465.css 14.4 kB
98
- ├ λ /pvt/account/orders/[id] 13.6 kB 171 kB
98
+ ├ λ /pvt/account/orders/[id] 13.6 kB 172 kB
99
99
  ├ └ css/482e6fa149885bc2.css 13.9 kB
100
100
  ├ λ /pvt/account/profile 1.99 kB 160 kB
101
101
  ├ └ css/5cd37b7508dd5190.css 4.56 kB
@@ -103,8 +103,8 @@ Route (pages) Size First Load JS
103
103
  + First Load JS shared by all 118 kB
104
104
  ├ chunks/framework-d514426edf885c68.js 45.4 kB
105
105
  ├ chunks/main-edeb2c6a5c48bb3f.js 33.2 kB
106
- ├ chunks/pages/_app-8be996b3105d8c26.js 26.3 kB
107
- ├ chunks/webpack-fd706604cbbdc60f.js 3.88 kB
106
+ ├ chunks/pages/_app-87b58cddac61876b.js 26.6 kB
107
+ ├ chunks/webpack-da79ff7bed51ca28.js 3.88 kB
108
108
  └ css/8a513f4b536b0ab0.css 9.18 kB
109
109
 
110
110
  λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
@@ -1,16 +1,17 @@
1
1
 
2
- > @faststore/core@3.99.0-dev.0 test /home/runner/work/faststore/faststore/packages/core
2
+ > @faststore/core@3.99.0-dev.3 test /home/runner/work/faststore/faststore/packages/core
3
3
  > jest
4
4
 
5
- PASS test/utils/multipleTemplates.test.ts (12.424 s)
6
- PASS test/utils/clearCookies.test.ts (11.298 s)
7
- PASS test/utils/cookieCacheBusting.test.ts (13.871 s)
5
+ PASS test/utils/multipleTemplates.test.ts (13.623 s)
6
+ PASS test/utils/clearCookies.test.ts (13.117 s)
7
+ PASS test/utils/cookieCacheBusting.test.ts (15.41 s)
8
8
  PASS test/server/cms/global.test.ts
9
- PASS test/server/cms/index.test.ts (5.532 s)
10
- PASS test/server/index.test.ts (12.61 s)
9
+ PASS test/utils/validateSessionRefreshToken.test.ts
10
+ PASS test/server/cms/index.test.ts (5.883 s)
11
+ PASS test/server/index.test.ts (12.391 s)
11
12
 
12
- Test Suites: 6 passed, 6 total
13
- Tests: 52 passed, 52 total
13
+ Test Suites: 7 passed, 7 total
14
+ Tests: 58 passed, 58 total
14
15
  Snapshots: 0 total
15
- Time: 26.233 s
16
+ Time: 27.436 s
16
17
  Ran all test suites.
package/@generated/gql.ts CHANGED
@@ -62,7 +62,7 @@ const documents = {
62
62
  types.ProcessOrderAuthorizationMutationDocument,
63
63
  '\n query ValidateUser {\n validateUser {\n isValid\n }\n }\n':
64
64
  types.ValidateUserDocument,
65
- '\n mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) {\n validateCart(cart: $cart, session: $session) {\n order {\n orderNumber\n acceptedOffer {\n ...CartItem\n }\n shouldSplitItem\n }\n messages {\n ...CartMessage\n }\n }\n }\n\n fragment CartMessage on StoreCartMessage {\n text\n status\n }\n\n fragment CartItem on StoreOffer {\n seller {\n identifier\n }\n quantity\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n itemOffered {\n ...CartProductItem\n }\n }\n\n fragment CartProductItem on StoreProduct {\n sku\n name\n unitMultiplier\n image {\n url\n alternateName\n }\n brand {\n name\n }\n isVariantOf {\n productGroupID\n name\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n gtin\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n':
65
+ '\n mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) {\n validateCart(cart: $cart, session: $session) {\n order {\n orderNumber\n acceptedOffer {\n ...CartItem\n }\n shouldSplitItem\n }\n messages {\n ...CartMessage\n }\n }\n }\n\n fragment CartMessage on StoreCartMessage {\n text\n status\n }\n\n fragment CartItem on StoreOffer {\n seller {\n identifier\n }\n quantity\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n isGift\n itemOffered {\n ...CartProductItem\n }\n }\n\n fragment CartProductItem on StoreProduct {\n sku\n name\n unitMultiplier\n image {\n url\n alternateName\n }\n brand {\n name\n }\n isVariantOf {\n productGroupID\n name\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n gtin\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n':
66
66
  types.ValidateCartMutationDocument,
67
67
  '\n query ClientPickupPointsQuery(\n $geoCoordinates: IStoreGeoCoordinates\n ) {\n pickupPoints(geoCoordinates: $geoCoordinates) {\n pickupPointDistances {\n pickupId\n distance\n pickupName\n isActive\n address {\n city\n state\n number\n postalCode\n street\n }\n }\n }\n }\n':
68
68
  types.ClientPickupPointsQueryDocument,
@@ -250,7 +250,7 @@ export function gql(
250
250
  * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
251
251
  */
252
252
  export function gql(
253
- source: '\n mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) {\n validateCart(cart: $cart, session: $session) {\n order {\n orderNumber\n acceptedOffer {\n ...CartItem\n }\n shouldSplitItem\n }\n messages {\n ...CartMessage\n }\n }\n }\n\n fragment CartMessage on StoreCartMessage {\n text\n status\n }\n\n fragment CartItem on StoreOffer {\n seller {\n identifier\n }\n quantity\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n itemOffered {\n ...CartProductItem\n }\n }\n\n fragment CartProductItem on StoreProduct {\n sku\n name\n unitMultiplier\n image {\n url\n alternateName\n }\n brand {\n name\n }\n isVariantOf {\n productGroupID\n name\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n gtin\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n'
253
+ source: '\n mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) {\n validateCart(cart: $cart, session: $session) {\n order {\n orderNumber\n acceptedOffer {\n ...CartItem\n }\n shouldSplitItem\n }\n messages {\n ...CartMessage\n }\n }\n }\n\n fragment CartMessage on StoreCartMessage {\n text\n status\n }\n\n fragment CartItem on StoreOffer {\n seller {\n identifier\n }\n quantity\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n isGift\n itemOffered {\n ...CartProductItem\n }\n }\n\n fragment CartProductItem on StoreProduct {\n sku\n name\n unitMultiplier\n image {\n url\n alternateName\n }\n brand {\n name\n }\n isVariantOf {\n productGroupID\n name\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n gtin\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n'
254
254
  ): typeof import('./graphql').ValidateCartMutationDocument
255
255
  /**
256
256
  * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
@@ -1348,6 +1348,8 @@ export type StoreMarketingData = {
1348
1348
  export type StoreOffer = {
1349
1349
  /** Offer item availability. */
1350
1350
  availability: Scalars['String']['output']
1351
+ /** Whether this offer is a gift (e.g. free promotional item). */
1352
+ isGift: Maybe<Scalars['Boolean']['output']>
1351
1353
  /** Offer item condition. */
1352
1354
  itemCondition: Scalars['String']['output']
1353
1355
  /** Information on the item being offered. */
@@ -3143,6 +3145,7 @@ export type ValidateCartMutationMutation = {
3143
3145
  priceWithTaxes: number
3144
3146
  listPrice: number
3145
3147
  listPriceWithTaxes: number
3148
+ isGift: boolean | null
3146
3149
  seller: { identifier: string }
3147
3150
  itemOffered: {
3148
3151
  sku: string
@@ -3181,6 +3184,7 @@ export type CartItemFragment = {
3181
3184
  priceWithTaxes: number
3182
3185
  listPrice: number
3183
3186
  listPriceWithTaxes: number
3187
+ isGift: boolean | null
3184
3188
  seller: { identifier: string }
3185
3189
  itemOffered: {
3186
3190
  sku: string
@@ -4390,6 +4394,7 @@ export const CartItemFragmentDoc = new TypedDocumentString(
4390
4394
  priceWithTaxes
4391
4395
  listPrice
4392
4396
  listPriceWithTaxes
4397
+ isGift
4393
4398
  itemOffered {
4394
4399
  ...CartProductItem
4395
4400
  }
@@ -4536,7 +4541,7 @@ export const ValidateUserDocument = {
4536
4541
  export const ValidateCartMutationDocument = {
4537
4542
  __meta__: {
4538
4543
  operationName: 'ValidateCartMutation',
4539
- operationHash: 'c2b3f8bff73ebf6ac79d758c66cabbc21ba9fcc0',
4544
+ operationHash: '32c15f8888ca34f223def7972b7f19090808435a',
4540
4545
  },
4541
4546
  } as unknown as TypedDocumentString<
4542
4547
  ValidateCartMutationMutation,
@@ -10,7 +10,7 @@
10
10
  "e2b06da6840614d3c72768e56579b9d3b8e80802": "mutation CancelOrderMutation($data: IUserOrderCancel!) { cancelOrder(data: $data) { data } }",
11
11
  "8c25d37c8d6e7c20ab21bb8a4f4e6a2fe320ea8d": "mutation ProcessOrderAuthorizationMutation($data: IProcessOrderAuthorization!) { processOrderAuthorization(data: $data) { isPendingForOtherAuthorizer ruleForAuthorization { dimensionId orderAuthorizationId rule { authorizationData { authorizers { authorizationDate email id type } requireAllApprovals } authorizedEmails doId id isUserAuthorized isUserNextAuthorizer name notification priority scoreInterval { accept deny } status timeout trigger { condition { conditionType description expression greatherThan lessThan } effect { description effectType funcPath } } } } } }",
12
12
  "32f99c73c3de958b64d6bece1afe800469f54548": "query ValidateUser { validateUser { isValid } }",
13
- "c2b3f8bff73ebf6ac79d758c66cabbc21ba9fcc0": "fragment CartItem on StoreOffer { itemOffered { ...CartProductItem } listPrice listPriceWithTaxes price priceWithTaxes quantity seller { identifier } } fragment CartMessage on StoreCartMessage { status text } fragment CartProductItem on StoreProduct { additionalProperty { name propertyID value valueReference } brand { name } gtin image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations availableVariations slugsMap } } name sku unitMultiplier } mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) { validateCart(cart: $cart, session: $session) { messages { ...CartMessage } order { acceptedOffer { ...CartItem } orderNumber shouldSplitItem } } }",
13
+ "32c15f8888ca34f223def7972b7f19090808435a": "fragment CartItem on StoreOffer { isGift itemOffered { ...CartProductItem } listPrice listPriceWithTaxes price priceWithTaxes quantity seller { identifier } } fragment CartMessage on StoreCartMessage { status text } fragment CartProductItem on StoreProduct { additionalProperty { name propertyID value valueReference } brand { name } gtin image { alternateName url } isVariantOf { name productGroupID skuVariants { activeVariations availableVariations slugsMap } } name sku unitMultiplier } mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) { validateCart(cart: $cart, session: $session) { messages { ...CartMessage } order { acceptedOffer { ...CartItem } orderNumber shouldSplitItem } } }",
14
14
  "3fa04e88c811fcb5ece7206fd5aa745bdbc143a8": "query ClientPickupPointsQuery($geoCoordinates: IStoreGeoCoordinates) { pickupPoints(geoCoordinates: $geoCoordinates) { pickupPointDistances { address { city number postalCode state street } distance isActive pickupId pickupName } } }",
15
15
  "feb7005103a859e2bc8cf2360d568806fd88deba": "mutation SubscribeToNewsletter($data: IPersonNewsletter!) { subscribeToNewsletter(data: $data) { id } }",
16
16
  "dc912e7272e3d9f5ced206837df87f544d39d0a5": "query ClientProductCountQuery($term: String) { productCount(term: $term) { total } }",
@@ -288,6 +288,8 @@ type StoreOffer {
288
288
  itemOffered: StoreProduct!
289
289
  """Number of items offered."""
290
290
  quantity: Int!
291
+ """Whether this offer is a gift (e.g. free promotional item)."""
292
+ isGift: Boolean
291
293
  }
292
294
 
293
295
  """Offer input."""
package/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.99.0-dev.4 (2026-04-28)
7
+
8
+ ### Bug Fixes
9
+
10
+ - Refactor token refresh logic in GraphQL API handler - SFS-3104 ([#3263](https://github.com/vtex/faststore/issues/3263)) ([695ab6a](https://github.com/vtex/faststore/commit/695ab6a9413eb53ef457cfc890c74e41c29c1ab8))
11
+
12
+ # 3.99.0-dev.3 (2026-04-23)
13
+
14
+ ### Features
15
+
16
+ - add isGift field to StoreOffer type and resolvers - SFS-3040 ([#3220](https://github.com/vtex/faststore/issues/3220)) ([e10fc92](https://github.com/vtex/faststore/commit/e10fc92633a5296a5b03286028d31d9c280561c6))
17
+ - Enhance session management with logout - SFS-3105 ([#3268](https://github.com/vtex/faststore/issues/3268)) ([9468e32](https://github.com/vtex/faststore/commit/9468e32a3531ca3c91c7f82a34ac1c750c6a26ef))
18
+
19
+ # [3.99.0-dev.2](https://github.com/vtex/faststore/compare/v3.99.0-dev.1...v3.99.0-dev.2) (2026-04-21)
20
+
21
+ **Note:** Version bump only for package @faststore/core
22
+
6
23
  # [3.99.0-dev.1](https://github.com/vtex/faststore/compare/v3.99.0-dev.0...v3.99.0-dev.1) (2026-04-15)
7
24
 
8
25
  **Note:** Version bump only for package @faststore/core
@@ -148,6 +148,7 @@ module.exports = {
148
148
  enableRedirects: false,
149
149
  enableSearchSSR: false,
150
150
  enableFaststoreMyAccount: false,
151
+ useIsGiftFromOrderForm: false,
151
152
  graphqlCacheControl: {
152
153
  maxAge: 0, // 0 disables cache, 5 * 60 enable cache control maxAge 5 minutes
153
154
  staleWhileRevalidate: 60 * 60, // 1 hour
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/core",
3
- "version": "3.99.0-dev.1",
3
+ "version": "3.99.0-dev.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -51,11 +51,11 @@
51
51
  "@envelop/graphql-jit": "^8.0.3",
52
52
  "@envelop/parser-cache": "^6.0.2",
53
53
  "@envelop/validation-cache": "^6.0.2",
54
- "@faststore/api": "3.98.1-dev.0",
55
- "@faststore/graphql-utils": "^3.98.1-dev.0",
56
- "@faststore/lighthouse": "3.98.1-dev.0",
57
- "@faststore/sdk": "3.98.1-dev.0",
58
- "@faststore/ui": "3.99.0-dev.1",
54
+ "@faststore/api": "3.99.0-dev.4",
55
+ "@faststore/graphql-utils": "^3.99.0-dev.4",
56
+ "@faststore/lighthouse": "3.99.0-dev.4",
57
+ "@faststore/sdk": "3.99.0-dev.4",
58
+ "@faststore/ui": "3.99.0-dev.4",
59
59
  "@graphql-codegen/cli": "5.0.2",
60
60
  "@graphql-codegen/client-preset": "4.2.6",
61
61
  "@graphql-codegen/typescript": "4.0.7",
@@ -124,5 +124,5 @@
124
124
  "ts-jest": "29.1.1",
125
125
  "typescript": "5.3.2"
126
126
  },
127
- "gitHead": "ea8071b1888cc8fa2092fd2f2b4927c7d0ff09c0"
127
+ "gitHead": "3069a16c7894cfe1f1da44e4dd812080e074d616"
128
128
  }
@@ -12,7 +12,10 @@ import { useEffect } from 'react'
12
12
  * We handle both: check the flag on mount (fresh load) and on pageshow (bfcache).
13
13
  */
14
14
 
15
- export const RELOAD_AFTER_LOGOUT_KEY = 'faststore_reload_after_logout_return'
15
+ import {
16
+ RELOAD_AFTER_LOGOUT_KEY,
17
+ SESSION_READY_KEY,
18
+ } from '../../../../sdk/session/storageKeys'
16
19
 
17
20
  /**
18
21
  * Call before redirecting to logout. When the user returns to the store, the app
@@ -43,6 +46,8 @@ const checkAndReloadIfReturnedFromLogout = (): void => {
43
46
  try {
44
47
  if (sessionStorage.getItem(RELOAD_AFTER_LOGOUT_KEY)) {
45
48
  sessionStorage.removeItem(RELOAD_AFTER_LOGOUT_KEY)
49
+ // Pre-set session ready so the reloaded page starts without a skeleton.
50
+ sessionStorage.setItem(SESSION_READY_KEY, 'true')
46
51
  setTimeout(forceRefreshWithoutCache, RELOAD_DELAY_MS)
47
52
  }
48
53
  } catch {
@@ -8,7 +8,8 @@ import { parse } from 'cookie'
8
8
  import type { NextApiHandler, NextApiRequest } from 'next'
9
9
 
10
10
  import discoveryConfig from 'discovery.config'
11
- import { getJWTAutCookie, isExpired } from 'src/utils/getCookie'
11
+ import { getJWTAutCookie } from 'src/utils/getCookie'
12
+ import { shouldForceRefreshTokenForValidateSession } from 'src/utils/validateSessionRefreshToken'
12
13
  import { execute } from '../../server'
13
14
 
14
15
  const DEFAULT_MAX_AGE = 5 * 60 // 5 minutes
@@ -169,27 +170,10 @@ const handler: NextApiHandler = async (request, response) => {
169
170
  account: discoveryConfig.api.storeId,
170
171
  })
171
172
 
172
- const tokenExpired = Boolean(jwt && isExpired(Number(jwt?.exp)))
173
-
174
- const refreshAfterExist = !!variables?.session?.refreshAfter
175
-
176
- const refreshAfterExpired =
177
- refreshAfterExist && isExpired(Number(variables.session.refreshAfter))
178
-
179
- const tokenExistAndIsFirstRefreshTokenRequest =
180
- !!jwt && !refreshAfterExist
181
-
182
- // when token expired, browser clears the cookie, but we still have the refreshAfter in session and the refresh token cookie
183
- const tokenNotExistAndRefreshAfterExistAndIsExpired =
184
- !jwt && !!refreshAfterExist && refreshAfterExpired
185
-
186
- const tokenExpiredAndRefreshAfterIsNullOrExpired =
187
- tokenExpired && (!refreshAfterExist || refreshAfterExpired)
188
-
189
- const shouldRefreshToken =
190
- tokenExistAndIsFirstRefreshTokenRequest ||
191
- tokenNotExistAndRefreshAfterExistAndIsExpired ||
192
- tokenExpiredAndRefreshAfterIsNullOrExpired
173
+ const shouldRefreshToken = shouldForceRefreshTokenForValidateSession({
174
+ jwt,
175
+ sessionRefreshAfter: variables?.session?.refreshAfter,
176
+ })
193
177
 
194
178
  if (shouldRefreshToken) {
195
179
  throw new UnauthorizedError(
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react'
2
- import { sessionStore } from '../session'
2
+ import { logoutAndClearSession, sessionStore } from '../session'
3
3
  import { isRefreshTokenSuccessful, refreshTokenRequest } from './refreshToken'
4
4
 
5
5
  export const useRefreshToken = (
@@ -37,23 +37,12 @@ export const useRefreshToken = (
37
37
  url.searchParams.set('_refresh', Date.now().toString())
38
38
  window.location.href = url.toString()
39
39
  } else {
40
- // If refresh token failed, set refreshAfter to now + 1 hour
41
- sessionStore.set({
42
- ...currentSession,
43
- refreshAfter: String(Math.floor(Date.now() / 1000) + 1 * 60 * 60), // now + 1 hour
44
- })
45
-
40
+ await logoutAndClearSession(currentSession)
46
41
  setShouldShow403(true)
47
42
  }
48
43
  } catch (error) {
49
44
  console.error('Error during refresh token process:', error)
50
-
51
- // Set refreshAfter to postpone future requests and redirect to login
52
- sessionStore.set({
53
- ...currentSession,
54
- refreshAfter: String(Math.floor(Date.now() / 1000) + 1 * 60 * 60), // now + 1 hour
55
- })
56
-
45
+ await logoutAndClearSession(currentSession)
57
46
  setShouldShow403(true)
58
47
  }
59
48
  }
@@ -16,7 +16,11 @@ import { request } from '../graphql/request'
16
16
  import { sessionStore } from '../session'
17
17
  import { createValidationStore, useStore } from '../useStore'
18
18
 
19
- export interface CartItem extends SDKCartItem, CartItemFragment {}
19
+ export interface CartItem
20
+ extends SDKCartItem,
21
+ Omit<CartItemFragment, 'isGift'> {
22
+ isGift?: boolean | null
23
+ }
20
24
 
21
25
  export interface Cart extends SDKCart<CartItem> {
22
26
  messages?: CartMessageFragment[]
@@ -53,6 +57,7 @@ export const ValidateCartMutation = gql(`
53
57
  priceWithTaxes
54
58
  listPrice
55
59
  listPriceWithTaxes
60
+ isGift
56
61
  itemOffered {
57
62
  ...CartProductItem
58
63
  }
@@ -88,7 +93,12 @@ export const ValidateCartMutation = gql(`
88
93
  }
89
94
  `)
90
95
 
91
- const isGift = (item: CartItem) => item.price === 0
96
+ const isGift = (item: CartItem) => {
97
+ if (storeConfig.experimental?.useIsGiftFromOrderForm) {
98
+ return item?.isGift ?? false
99
+ }
100
+ return item.price === 0
101
+ }
92
102
 
93
103
  const getItemId = (item: Pick<CartItem, 'itemOffered' | 'seller' | 'price'>) =>
94
104
  [
@@ -1,6 +1,6 @@
1
1
  import type { Session } from '@faststore/sdk'
2
2
  import { createSessionStore } from '@faststore/sdk'
3
- import { useEffect, useMemo, useRef, useState } from 'react'
3
+ import { useEffect, useMemo, useState } from 'react'
4
4
 
5
5
  import { gql } from '@generated'
6
6
  import type {
@@ -17,8 +17,18 @@ import { cartStore } from '../cart'
17
17
  import { request } from '../graphql/request'
18
18
  import { createValidationStore, useStore } from '../useStore'
19
19
  import { getPostalCode } from '../userLocation/index'
20
+ import { RELOAD_AFTER_LOGOUT_KEY, SESSION_READY_KEY } from './storageKeys'
20
21
 
21
- const SESSION_READY_KEY = 'faststore_session_ready'
22
+ const isReloadAfterLogoutPending = (): boolean => {
23
+ try {
24
+ return (
25
+ typeof sessionStorage !== 'undefined' &&
26
+ !!sessionStorage.getItem(RELOAD_AFTER_LOGOUT_KEY)
27
+ )
28
+ } catch {
29
+ return false
30
+ }
31
+ }
22
32
 
23
33
  export const mutation = gql(`
24
34
  mutation ValidateSession($session: IStoreSession!, $search: String!) {
@@ -77,7 +87,50 @@ export const mutation = gql(`
77
87
  }
78
88
  `)
79
89
 
90
+ async function handleRefreshToken(session: Session): Promise<Session | null> {
91
+ const result = await refreshTokenRequest()
92
+
93
+ if (isRefreshTokenSuccessful(result)) {
94
+ return {
95
+ ...session,
96
+ refreshAfter: String(
97
+ Math.floor(new Date(result?.refreshAfter).getTime() / 1000)
98
+ ),
99
+ }
100
+ }
101
+
102
+ await logoutAndClearSession(session)
103
+ return null
104
+ }
105
+
106
+ function isRefreshAfterExpired(session: Session): boolean {
107
+ return (
108
+ !!session.refreshAfter &&
109
+ Math.floor(Date.now() / 1000) > Number(session.refreshAfter)
110
+ )
111
+ }
112
+
80
113
  export const validateSession = async (session: Session) => {
114
+ // Skip validation if the page is about to reload after logout — avoids an
115
+ // aborted-fetch TypeError when the forced reload navigates the page away.
116
+ if (isReloadAfterLogoutPending()) {
117
+ return null
118
+ }
119
+
120
+ // If the refreshToken is enabled and the refreshAfter is expired, refresh the token.
121
+ // On success, continue to the validation flow with the refreshed session.
122
+ // On failure (logoutAndClearSession already triggered), bail out.
123
+ if (
124
+ storeConfig.experimental?.refreshToken &&
125
+ isRefreshAfterExpired(session)
126
+ ) {
127
+ const refreshed = await handleRefreshToken(session)
128
+ if (!refreshed) {
129
+ return null
130
+ }
131
+ session = refreshed
132
+ }
133
+
81
134
  // If deliveryPromise is enabled and there is no postalCode in the session
82
135
  if (
83
136
  storeConfig.deliveryPromise?.enabled &&
@@ -123,30 +176,16 @@ export const validateSession = async (session: Session) => {
123
176
  error?.status === 401 && storeConfig.experimental?.refreshToken
124
177
 
125
178
  if (shouldRefreshToken) {
126
- const result = await refreshTokenRequest()
127
-
128
- if (isRefreshTokenSuccessful(result)) {
129
- const refreshAfter = String(
130
- Math.floor(new Date(result?.refreshAfter).getTime() / 1000)
131
- )
132
-
133
- sessionStore.set({
134
- ...session,
135
- refreshAfter,
136
- })
137
- } else {
138
- // If the refresh token fails 3x, set the refreshAfter to now + 1 hour
139
- // so that we can postpone refreshToken request and continue the ValidateSession request
140
- sessionStore.set({
141
- ...session,
142
- refreshAfter: String(Math.floor(Date.now() / 1000) + 1 * 60 * 60), // now + 1 hour
143
- })
179
+ const refreshed = await handleRefreshToken(session)
180
+ if (refreshed) {
181
+ sessionStore.set(refreshed)
144
182
  }
145
183
  }
146
184
  }
147
185
  }
148
186
 
149
- const [validationStore, onValidate] = createValidationStore(validateSession)
187
+ const [validationStore, onValidate, hasValidatedStore] =
188
+ createValidationStore(validateSession)
150
189
 
151
190
  const defaultStore = createSessionStore(storeConfig.session, onValidate)
152
191
 
@@ -162,6 +201,21 @@ export const sessionStore = {
162
201
  },
163
202
  }
164
203
 
204
+ export async function logoutAndClearSession(session: Session) {
205
+ try {
206
+ await fetch('/api/fs/logout', { method: 'POST' })
207
+ } catch (logoutError) {
208
+ console.error('Failed to call logout endpoint:', logoutError)
209
+ }
210
+
211
+ sessionStore.set({
212
+ ...session,
213
+ person: null,
214
+ b2b: null,
215
+ refreshAfter: null,
216
+ })
217
+ }
218
+
165
219
  interface SessionOptions {
166
220
  filter?: boolean
167
221
  }
@@ -184,7 +238,8 @@ export const useSession = ({ filter }: SessionOptions = { filter: true }) => {
184
238
  const currentSessionStore = sessionStore.read() ?? sessionStore.readInitial()
185
239
  const resultSessionStore = useStore(sessionStore)
186
240
  const isValidating = useStore(validationStore)
187
- const { isSessionReady } = useSessionReady({ isValidating })
241
+ const hasValidated = useStore(hasValidatedStore)
242
+ const { isSessionReady } = useSessionReady({ isValidating, hasValidated })
188
243
 
189
244
  let { channel, ...session } = resultSessionStore ?? currentSessionStore
190
245
 
@@ -211,12 +266,21 @@ export const useSession = ({ filter }: SessionOptions = { filter: true }) => {
211
266
  * The session ready state is persisted in sessionStorage to maintain consistency across page navigations
212
267
  * and provide faster loading times on subsequent page visits.
213
268
  *
269
+ * Uses `hasValidated` (a reactive store value) instead of a ref-based transition tracker to avoid
270
+ * a React 18 automatic batching race condition where `isValidating` goes true→false so fast that
271
+ * the effect never observes the `true` state, leaving `isSessionReady` stuck at false.
272
+ *
214
273
  * @param isValidating - Whether the session is currently being validated
274
+ * @param hasValidated - Whether at least one validation cycle has ever completed
215
275
  * @returns An object containing the session readiness status
216
276
  */
217
277
  export const useSessionReady = ({
218
278
  isValidating,
219
- }: { isValidating: boolean }) => {
279
+ hasValidated,
280
+ }: {
281
+ isValidating: boolean
282
+ hasValidated: boolean
283
+ }) => {
220
284
  // Initialize with persisted state from sessionStorage
221
285
  const [isSessionReady, setIsSessionReady] = useState(() => {
222
286
  if (typeof window === 'undefined') return false
@@ -227,44 +291,18 @@ export const useSessionReady = ({
227
291
  }
228
292
  })
229
293
 
230
- // Wait for session to be stable (not validating and has consistent data)
231
- const hasValidatedRef = useRef(false)
232
-
233
294
  useEffect(() => {
234
- // Only run the effect if there has already been a change in isValidating (that is, validateSession has already been called at least once)
235
- if (!hasValidatedRef.current && isValidating) {
236
- hasValidatedRef.current = true
237
- return
238
- }
239
-
240
- if (!hasValidatedRef.current) {
241
- return
242
- }
243
-
244
- if (!isValidating) {
245
- setIsSessionReady(true)
246
- // Persist the ready state in sessionStorage
247
- try {
248
- sessionStorage.setItem(SESSION_READY_KEY, 'true')
249
- } catch {
250
- // Ignore storage errors
251
- }
252
- return
253
- }
295
+ // Only mark ready once at least one full validation cycle has completed
296
+ // and validation is not currently in flight.
297
+ if (isValidating || !hasValidated) return
254
298
 
255
- // Only set to false if we don't have a persisted ready state
256
- if (typeof window !== 'undefined') {
257
- try {
258
- const persistedReady =
259
- sessionStorage.getItem(SESSION_READY_KEY) === 'true'
260
- if (!persistedReady) {
261
- setIsSessionReady(false)
262
- }
263
- } catch {
264
- setIsSessionReady(false)
265
- }
299
+ setIsSessionReady(true)
300
+ try {
301
+ sessionStorage.setItem(SESSION_READY_KEY, 'true')
302
+ } catch {
303
+ // Ignore storage errors
266
304
  }
267
- }, [isValidating])
305
+ }, [isValidating, hasValidated])
268
306
 
269
307
  return { isSessionReady }
270
308
  }
@@ -0,0 +1,2 @@
1
+ export const RELOAD_AFTER_LOGOUT_KEY = 'faststore_reload_after_logout_return'
2
+ export const SESSION_READY_KEY = 'faststore_session_ready'