@faststore/core 4.4.0-dev.3 → 4.4.0-dev.5

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 (31) hide show
  1. package/.turbo/turbo-generate.log +3 -3
  2. package/.turbo/turbo-test.log +49 -44
  3. package/CHANGELOG.md +12 -0
  4. package/README.md +15 -0
  5. package/package.json +6 -6
  6. package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx +1 -1
  7. package/src/components/account/Menu/Menu.tsx +2 -1
  8. package/src/components/account/orders/ListOrders/FilterSlider/FilterSlider.tsx +5 -1
  9. package/src/components/account/orders/ListOrders/ListOrders.tsx +8 -3
  10. package/src/components/account/orders/ListOrders/ListOrdersTable/ListOrdersTable.tsx +6 -3
  11. package/src/components/account/orders/ListOrders/SelectedTags/SelectedTags.tsx +2 -2
  12. package/src/components/account/orders/OrderDetails/OrderDetailsHeader.tsx +6 -1
  13. package/src/components/account/utils/useFormatPrice.ts +3 -2
  14. package/src/components/ui/Link/Link.tsx +6 -0
  15. package/src/experimental/index.ts +3 -0
  16. package/src/experimental/myAccountServerSideProps.ts +6 -2
  17. package/src/pages/pvt/account/404.tsx +5 -1
  18. package/src/pages/pvt/account/[...unknown].tsx +6 -2
  19. package/src/pages/pvt/account/index.tsx +5 -4
  20. package/src/pages/pvt/account/profile.tsx +2 -1
  21. package/src/pages/pvt/account/user-details.tsx +4 -3
  22. package/src/proxy.ts +36 -0
  23. package/src/sdk/session/index.ts +9 -6
  24. package/src/sdk/session/initialSession.ts +31 -46
  25. package/src/utils/localization/localizeRedirectDestination.ts +66 -0
  26. package/src/utils/localization/withLocaleValidation.ts +0 -6
  27. package/test/proxy.localization.test.ts +122 -0
  28. package/test/sdk/session/reconcileSessionLocale.browser.test.ts +199 -0
  29. package/test/sdk/session/reconcileSessionLocale.disabled.browser.test.ts +38 -0
  30. package/test/utils/localization/localizeRedirectDestination.test.ts +176 -0
  31. package/test/sdk/session/installLocaleCorrector.browser.test.ts +0 -208
@@ -0,0 +1,176 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ const mockStoreConfig = vi.hoisted(() => ({
4
+ localization: {
5
+ enabled: true,
6
+ defaultLocale: 'en-US',
7
+ locales: {
8
+ 'en-US': {
9
+ code: 'en-US',
10
+ bindings: [
11
+ {
12
+ url: 'https://brandless.fast.store/en-US',
13
+ salesChannel: '1',
14
+ isDefault: true,
15
+ },
16
+ ],
17
+ },
18
+ 'it-IT': {
19
+ code: 'it-IT',
20
+ bindings: [
21
+ {
22
+ url: 'https://brandless.fast.store/it-IT',
23
+ salesChannel: '1',
24
+ isDefault: true,
25
+ },
26
+ {
27
+ url: 'https://brandless.fast.store/europe/it',
28
+ salesChannel: '1',
29
+ isDefault: false,
30
+ },
31
+ ],
32
+ },
33
+ 'en-GB': {
34
+ code: 'en-GB',
35
+ bindings: [
36
+ {
37
+ url: 'https://brandless.fast.store/emea-eur',
38
+ salesChannel: '1',
39
+ isDefault: true,
40
+ },
41
+ ],
42
+ },
43
+ },
44
+ },
45
+ }))
46
+
47
+ vi.mock('../../../discovery.config.js', () => ({
48
+ default: mockStoreConfig,
49
+ }))
50
+
51
+ import { localizeRedirectDestination } from '../../../src/utils/localization/localizeRedirectDestination'
52
+
53
+ const PROFILE = '/pvt/account/profile'
54
+ const ORDERS = '/pvt/account/orders'
55
+
56
+ describe('localizeRedirectDestination', () => {
57
+ describe('when localization is disabled', () => {
58
+ it('returns the destination unchanged', () => {
59
+ mockStoreConfig.localization.enabled = false
60
+
61
+ expect(
62
+ localizeRedirectDestination(PROFILE, {
63
+ locale: 'it-IT',
64
+ defaultLocale: 'en-US',
65
+ req: { url: '/europe/it/pvt/account' },
66
+ })
67
+ ).toBe(PROFILE)
68
+
69
+ mockStoreConfig.localization.enabled = true
70
+ })
71
+ })
72
+
73
+ describe('non-internal destinations', () => {
74
+ it('leaves external URLs unchanged', () => {
75
+ expect(
76
+ localizeRedirectDestination('https://example.com/account', {
77
+ locale: 'it-IT',
78
+ defaultLocale: 'en-US',
79
+ })
80
+ ).toBe('https://example.com/account')
81
+ })
82
+
83
+ it('leaves protocol-relative URLs unchanged', () => {
84
+ expect(
85
+ localizeRedirectDestination('//cdn.example.com/path', {
86
+ locale: 'it-IT',
87
+ defaultLocale: 'en-US',
88
+ })
89
+ ).toBe('//cdn.example.com/path')
90
+ })
91
+ })
92
+
93
+ describe('custom binding paths (mirrors resolveLink + addCustomPathPrefix)', () => {
94
+ it('preserves /emea-eur when the request arrived on that vanity path', () => {
95
+ expect(
96
+ localizeRedirectDestination(PROFILE, {
97
+ locale: 'en-GB',
98
+ defaultLocale: 'en-US',
99
+ req: { url: '/emea-eur/pvt/account' },
100
+ })
101
+ ).toBe('/emea-eur/pvt/account/profile')
102
+ })
103
+
104
+ it('preserves /europe/it when the request arrived on that vanity path', () => {
105
+ expect(
106
+ localizeRedirectDestination(ORDERS, {
107
+ locale: 'it-IT',
108
+ defaultLocale: 'en-US',
109
+ req: { url: '/europe/it/pvt/account/profile' },
110
+ })
111
+ ).toBe('/europe/it/pvt/account/orders')
112
+ })
113
+
114
+ it('keeps query strings on the destination', () => {
115
+ expect(
116
+ localizeRedirectDestination('/pvt/account/403?from=%2Fpvt%2Faccount', {
117
+ locale: 'en-GB',
118
+ defaultLocale: 'en-US',
119
+ req: { url: '/emea-eur/pvt/account/profile' },
120
+ })
121
+ ).toBe('/emea-eur/pvt/account/403?from=%2Fpvt%2Faccount')
122
+ })
123
+ })
124
+
125
+ describe('Next.js locale-code paths', () => {
126
+ it('prepends /it-IT when locale is non-default and req.url has no custom prefix', () => {
127
+ expect(
128
+ localizeRedirectDestination(PROFILE, {
129
+ locale: 'it-IT',
130
+ defaultLocale: 'en-US',
131
+ req: { url: '/pvt/account' },
132
+ })
133
+ ).toBe('/it-IT/pvt/account/profile')
134
+ })
135
+
136
+ it('does not double-prefix when destination already includes the locale', () => {
137
+ expect(
138
+ localizeRedirectDestination('/it-IT/pvt/account/profile', {
139
+ locale: 'it-IT',
140
+ defaultLocale: 'en-US',
141
+ req: { url: '/pvt/account' },
142
+ })
143
+ ).toBe('/it-IT/pvt/account/profile')
144
+ })
145
+ })
146
+
147
+ describe('default locale and missing request context', () => {
148
+ it('returns destination unchanged for the default locale', () => {
149
+ expect(
150
+ localizeRedirectDestination(PROFILE, {
151
+ locale: 'en-US',
152
+ defaultLocale: 'en-US',
153
+ req: { url: '/pvt/account' },
154
+ })
155
+ ).toBe(PROFILE)
156
+ })
157
+
158
+ it('falls back to locale prefix when req is absent (getStaticProps)', () => {
159
+ expect(
160
+ localizeRedirectDestination('/pvt/account/404', {
161
+ locale: 'it-IT',
162
+ defaultLocale: 'en-US',
163
+ })
164
+ ).toBe('/it-IT/pvt/account/404')
165
+ })
166
+
167
+ it('returns destination unchanged when req is absent and locale is default', () => {
168
+ expect(
169
+ localizeRedirectDestination('/pvt/account/404', {
170
+ locale: 'en-US',
171
+ defaultLocale: 'en-US',
172
+ })
173
+ ).toBe('/pvt/account/404')
174
+ })
175
+ })
176
+ })
@@ -1,208 +0,0 @@
1
- /**
2
- * Tests for the localization race-condition fix.
3
- *
4
- * The `optimistic` middleware enqueues `onValidate(value)` as a microtask
5
- * (capturing a `cancel` flag in closure). When the persisted middleware
6
- * hydrates the store from IDB with a stale locale, the corrector subscriber
7
- * (installed after `optimistic`) detects the mismatch and re-enters
8
- * `store.set(corrected)`. The base store cancels in-flight optimistic handlers
9
- * via `cancelations.forEach(cancel)` BEFORE the stale handler ever runs,
10
- * preventing the stale `validateSession` from hitting the network.
11
- *
12
- * We deliberately skip `persisted` here and simulate IDB hydration by calling
13
- * `store.set(stale)` directly: persisted's only effect for this test is to
14
- * trigger that same set() asynchronously, which would only add timing noise.
15
- */
16
- import type { Session } from '@faststore/sdk'
17
- import { compose, createBaseStore, optimistic } from '@faststore/sdk'
18
- import { describe, expect, it, vi } from 'vitest'
19
-
20
- import { installLocaleCorrector } from '../../../src/sdk/session/initialSession'
21
-
22
- /**
23
- * Yields to a macrotask boundary, which guarantees all currently-enqueued
24
- * microtasks (the `optimistic` middleware's `queue.then(handler)` cascade)
25
- * have fully drained — including chained continuations from `await` points
26
- * inside async handlers. More deterministic than a fixed `sleep(N)` and more
27
- * robust than `await Promise.resolve()` chains (which only drain a fixed
28
- * number of microtask "generations").
29
- */
30
- const flushMicrotasks = () =>
31
- new Promise<void>((resolve) => setTimeout(resolve, 0))
32
-
33
- const URL_AWARE: Session = {
34
- locale: 'pt-BR',
35
- currency: { code: 'BRL', symbol: 'R$' },
36
- channel: '{"salesChannel":"1","regionId":"region-abc"}',
37
- country: 'BRA',
38
- deliveryMode: null,
39
- addressType: null,
40
- city: null,
41
- postalCode: null,
42
- geoCoordinates: null,
43
- b2b: null,
44
- person: null,
45
- marketingData: null,
46
- refreshAfter: null,
47
- }
48
-
49
- const STALE: Session = {
50
- ...URL_AWARE,
51
- locale: 'en-US',
52
- currency: { code: 'USD', symbol: '$' },
53
- channel: '{"salesChannel":"2","regionId":"region-xyz"}',
54
- }
55
-
56
- /**
57
- * NOTE on `onValidate` returning `null`:
58
- * The `optimistic` middleware re-enters `store.set(validated)` whenever
59
- * `onValidate` returns a non-null value, which would cause an infinite loop in
60
- * tests (see `packages/sdk/test/store/optimistic.test.ts` for the same
61
- * pattern). Returning `null` short-circuits that re-entry while still letting
62
- * us inspect the arguments the handler was called with.
63
- */
64
- function makeStore(initial: Session) {
65
- const onValidate = vi.fn(async (_: Session): Promise<Session | null> => null)
66
- const store = compose([optimistic(onValidate)], createBaseStore(initial))
67
- return { store, onValidate }
68
- }
69
-
70
- describe('installLocaleCorrector', () => {
71
- it('cancels stale validation and emits only one with the corrected locale on IDB hydration with stale data', async () => {
72
- const { store, onValidate } = makeStore(URL_AWARE)
73
- installLocaleCorrector(store, URL_AWARE)
74
-
75
- // Simulates `persisted.hydrateFromIDB` resolving with a stale payload.
76
- store.set(STALE)
77
- await flushMicrotasks()
78
-
79
- expect(onValidate).toHaveBeenCalledTimes(1)
80
- expect(onValidate.mock.calls[0][0].locale).toBe('pt-BR')
81
- expect(onValidate.mock.calls[0][0].currency).toEqual({
82
- code: 'BRL',
83
- symbol: 'R$',
84
- })
85
-
86
- // Local store state should also reflect the corrected locale.
87
- expect(store.read().locale).toBe('pt-BR')
88
- })
89
-
90
- it('preserves unrelated session fields from the hydrated payload (e.g. person)', async () => {
91
- const { store } = makeStore(URL_AWARE)
92
- installLocaleCorrector(store, URL_AWARE)
93
-
94
- const stalePayload: Session = {
95
- ...STALE,
96
- person: {
97
- id: 'p1',
98
- email: 'a@b.com',
99
- givenName: 'A',
100
- familyName: 'B',
101
- },
102
- }
103
- store.set(stalePayload)
104
- await flushMicrotasks()
105
-
106
- const corrected = store.read()
107
- expect(corrected.locale).toBe('pt-BR')
108
- expect(corrected.person).toEqual({
109
- id: 'p1',
110
- email: 'a@b.com',
111
- givenName: 'A',
112
- familyName: 'B',
113
- })
114
- })
115
-
116
- it('replaces salesChannel inside channel JSON while preserving non-locale channel fields from the hydrated payload', async () => {
117
- const { store } = makeStore(URL_AWARE)
118
- installLocaleCorrector(store, URL_AWARE)
119
-
120
- const stalePayload: Session = {
121
- ...STALE,
122
- channel:
123
- '{"salesChannel":"2","regionId":"region-xyz","customKey":"keep-me"}',
124
- }
125
- store.set(stalePayload)
126
- await flushMicrotasks()
127
-
128
- const channel = JSON.parse(store.read().channel ?? '{}')
129
- expect(channel.salesChannel).toBe('1')
130
- expect(channel.regionId).toBe('region-xyz')
131
- expect(channel.customKey).toBe('keep-me')
132
- })
133
-
134
- it('does not trigger an extra correction when IDB hydrates with a matching locale', async () => {
135
- const { store, onValidate } = makeStore(URL_AWARE)
136
- installLocaleCorrector(store, URL_AWARE)
137
-
138
- // Simulates IDB hydration with the SAME locale as the URL-aware default.
139
- store.set(URL_AWARE)
140
- await flushMicrotasks()
141
-
142
- expect(onValidate).toHaveBeenCalledTimes(1)
143
- expect(onValidate.mock.calls[0][0].locale).toBe('pt-BR')
144
- expect(store.read().locale).toBe('pt-BR')
145
- })
146
-
147
- it('only fires once: subsequent user-driven locale changes are NOT intercepted', async () => {
148
- const { store, onValidate } = makeStore(URL_AWARE)
149
- installLocaleCorrector(store, URL_AWARE)
150
-
151
- // First hydration triggers the corrector.
152
- store.set(STALE)
153
- await flushMicrotasks()
154
- expect(store.read().locale).toBe('pt-BR')
155
- onValidate.mockClear()
156
-
157
- // Now simulate a user changing locale via LocalizationSelector.
158
- store.set(STALE)
159
- await flushMicrotasks()
160
-
161
- // The corrector must NOT undo this user-driven change.
162
- expect(store.read().locale).toBe('en-US')
163
- expect(onValidate).toHaveBeenCalledTimes(1)
164
- expect(onValidate.mock.calls[0][0].locale).toBe('en-US')
165
- })
166
-
167
- it('returns an unsubscribe function that prevents future corrections', async () => {
168
- const { store, onValidate } = makeStore(URL_AWARE)
169
- const unsubscribe = installLocaleCorrector(store, URL_AWARE)
170
- unsubscribe()
171
-
172
- store.set(STALE)
173
- await flushMicrotasks()
174
-
175
- // Without the corrector active, the stale value stays in the store.
176
- expect(store.read().locale).toBe('en-US')
177
- expect(onValidate).toHaveBeenCalledTimes(1)
178
- expect(onValidate.mock.calls[0][0].locale).toBe('en-US')
179
- })
180
-
181
- it('survives a malformed channel JSON in the hydrated payload (does not throw, still corrects)', async () => {
182
- const { store, onValidate } = makeStore(URL_AWARE)
183
- installLocaleCorrector(store, URL_AWARE)
184
-
185
- const malformedPayload: Session = {
186
- ...STALE,
187
- // Simulates a corrupted IDB write — invalid JSON.
188
- channel: '{not valid json',
189
- }
190
-
191
- // If the corrector threw, the surrounding `store.set` would abort the
192
- // subscribers iteration and the optimistic handler queued for the stale
193
- // payload would NOT be cancelled — re-introducing the race condition.
194
- expect(() => store.set(malformedPayload)).not.toThrow()
195
- await flushMicrotasks()
196
-
197
- // Correction still applied: locale aligned with URL, salesChannel taken
198
- // from the URL-aware initial session (since the malformed channel is
199
- // treated as `{}`).
200
- expect(store.read().locale).toBe('pt-BR')
201
- const channel = JSON.parse(store.read().channel ?? '{}')
202
- expect(channel.salesChannel).toBe('1')
203
-
204
- // And the stale validation was cancelled — only the corrected one ran.
205
- expect(onValidate).toHaveBeenCalledTimes(1)
206
- expect(onValidate.mock.calls[0][0].locale).toBe('pt-BR')
207
- })
208
- })