Skip to content

Commit eb0445c

Browse files
committed
Address base path duplication
1 parent 975fbd9 commit eb0445c

File tree

3 files changed

+97
-7
lines changed

3 files changed

+97
-7
lines changed

packages/commerce-sdk-react/src/components/StorefrontPreview/storefront-preview.test.tsx

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,16 @@ import {useCommerceApi, useConfig} from '../../hooks'
1414

1515
declare global {
1616
interface Window {
17-
STOREFRONT_PREVIEW?: Record<string, unknown>
17+
STOREFRONT_PREVIEW?: {
18+
getToken?: () => string | undefined | Promise<string | undefined>
19+
onContextChange?: () => void | Promise<void>
20+
siteId?: string
21+
experimentalUnsafeNavigate?: (
22+
path: string | {pathname: string; search?: string; hash?: string; state?: unknown},
23+
action?: 'push' | 'replace',
24+
...args: unknown[]
25+
) => void
26+
}
1827
}
1928
}
2029

@@ -28,9 +37,24 @@ jest.mock('./utils', () => {
2837
jest.mock('../../auth/index.ts')
2938
jest.mock('../../hooks/useConfig', () => jest.fn())
3039

40+
const mockPush = jest.fn()
41+
const mockReplace = jest.fn()
42+
jest.mock('react-router-dom', () => {
43+
const actual = jest.requireActual('react-router-dom')
44+
return {
45+
...actual,
46+
useHistory: () => ({
47+
push: mockPush,
48+
replace: mockReplace
49+
})
50+
}
51+
})
52+
3153
describe('Storefront Preview Component', function () {
3254
beforeEach(() => {
3355
delete window.STOREFRONT_PREVIEW
56+
mockPush.mockClear()
57+
mockReplace.mockClear()
3458
;(useConfig as jest.Mock).mockReturnValue({siteId: 'site-id'})
3559
})
3660
afterEach(() => {
@@ -107,6 +131,40 @@ describe('Storefront Preview Component', function () {
107131
expect(window.STOREFRONT_PREVIEW?.experimentalUnsafeNavigate).toBeDefined()
108132
})
109133

134+
test('experimentalUnsafeNavigate removes base path from path when getBasePath is provided', () => {
135+
;(detectStorefrontPreview as jest.Mock).mockReturnValue(true)
136+
137+
render(
138+
<StorefrontPreview
139+
enabled={true}
140+
getToken={() => 'my-token'}
141+
getBasePath={() => '/mybase'}
142+
/>
143+
)
144+
145+
window.STOREFRONT_PREVIEW?.experimentalUnsafeNavigate?.('/mybase/product/123', 'push')
146+
expect(mockPush).toHaveBeenCalledWith('/product/123')
147+
148+
mockPush.mockClear()
149+
window.STOREFRONT_PREVIEW?.experimentalUnsafeNavigate?.('/mybase/account', 'replace')
150+
expect(mockReplace).toHaveBeenCalledWith('/account')
151+
})
152+
153+
test('experimentalUnsafeNavigate does not remove when path does not start with base path', () => {
154+
;(detectStorefrontPreview as jest.Mock).mockReturnValue(true)
155+
156+
render(
157+
<StorefrontPreview
158+
enabled={true}
159+
getToken={() => 'my-token'}
160+
getBasePath={() => '/mybase'}
161+
/>
162+
)
163+
164+
window.STOREFRONT_PREVIEW?.experimentalUnsafeNavigate?.('/other/product/123', 'push')
165+
expect(mockPush).toHaveBeenCalledWith('/other/product/123')
166+
})
167+
110168
test('cache breaker is added to the parameters of SCAPI requests, only if in storefront preview', () => {
111169
;(detectStorefrontPreview as jest.Mock).mockReturnValue(true)
112170
mockQueryEndpoint('baskets/123', {})

packages/commerce-sdk-react/src/components/StorefrontPreview/storefront-preview.tsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,49 @@ type GetToken = () => string | undefined | Promise<string | undefined>
1717
type ContextChangeHandler = () => void | Promise<void>
1818
type OptionalWhenDisabled<T> = ({enabled?: true} & T) | ({enabled: false} & Partial<T>)
1919

20+
/**
21+
* Strip the base path from a path
22+
*
23+
* React Router history re-adds the base path to the path, so we
24+
* remove it here to avoid base path duplication.
25+
*/
26+
function removeBasePathFromLocation<T>(
27+
pathOrLocation: LocationDescriptor<T>,
28+
basePath: string
29+
): LocationDescriptor<T> {
30+
if (!basePath) return pathOrLocation
31+
if (typeof pathOrLocation === 'string') {
32+
return pathOrLocation.startsWith(basePath)
33+
? pathOrLocation.slice(basePath.length) || '/'
34+
: pathOrLocation
35+
}
36+
const pathname = pathOrLocation.pathname ?? '/'
37+
const strippedPathname = pathname.startsWith(basePath)
38+
? pathname.slice(basePath.length) || '/'
39+
: pathname
40+
return {...pathOrLocation, pathname: strippedPathname}
41+
}
42+
2043
/**
2144
*
2245
* @param enabled - flag to turn on/off Storefront Preview feature. By default, it is set to true.
2346
* This flag only applies if storefront is running in a Runtime Admin iframe.
2447
* @param getToken - A method that returns the access token for the current user
48+
* @param getBasePath - A method that returns the router base path of the app.
2549
*/
2650
export const StorefrontPreview = ({
2751
children,
2852
enabled = true,
2953
getToken,
30-
onContextChange
54+
onContextChange,
55+
getBasePath
3156
}: React.PropsWithChildren<
3257
// Props are only required when Storefront Preview is enabled
33-
OptionalWhenDisabled<{getToken: GetToken; onContextChange?: ContextChangeHandler}>
58+
OptionalWhenDisabled<{
59+
getToken: GetToken
60+
onContextChange?: ContextChangeHandler
61+
getBasePath?: () => string
62+
}>
3463
>) => {
3564
const history = useHistory()
3665
const isHostTrusted = detectStorefrontPreview()
@@ -49,11 +78,13 @@ export const StorefrontPreview = ({
4978
action: 'push' | 'replace' = 'push',
5079
...args: unknown[]
5180
) => {
52-
history[action](path, ...args)
81+
const basePath = getBasePath?.() ?? ''
82+
const pathWithoutBase = removeBasePathFromLocation(path, basePath)
83+
history[action](pathWithoutBase, ...args)
5384
}
5485
}
5586
}
56-
}, [enabled, getToken, onContextChange, siteId])
87+
}, [enabled, getToken, onContextChange, siteId, getBasePath])
5788

5889
useEffect(() => {
5990
if (enabled && isHostTrusted) {
@@ -99,7 +130,8 @@ StorefrontPreview.propTypes = {
99130
// to get to a place where both these props are simply optional and we will provide default implementations.
100131
// This would make the API simpler to use.
101132
getToken: CustomPropTypes.requiredFunctionWhenEnabled,
102-
onContextChange: PropTypes.func
133+
onContextChange: PropTypes.func,
134+
getBasePath: PropTypes.func
103135
}
104136

105137
export default StorefrontPreview

packages/template-retail-react-app/app/components/_app/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ const App = (props) => {
303303

304304
return (
305305
<Box className="sf-app" {...styles.container}>
306-
<StorefrontPreview getToken={getTokenWhenReady}>
306+
<StorefrontPreview getToken={getTokenWhenReady} getBasePath={getRouterBasePath}>
307307
<IntlProvider
308308
onError={(err) => {
309309
if (!messages) {

0 commit comments

Comments
 (0)