-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathstorefront-preview.tsx
More file actions
145 lines (135 loc) · 5.1 KB
/
storefront-preview.tsx
File metadata and controls
145 lines (135 loc) · 5.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/*
* Copyright (c) 2023, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React, {useEffect} from 'react'
import PropTypes from 'prop-types'
import {Helmet} from 'react-helmet'
import {CustomPropTypes, detectStorefrontPreview, getClientScript, proxyRequests} from './utils'
import {useHistory} from 'react-router-dom'
import type {LocationDescriptor} from 'history'
import {useCommerceApi, useConfig} from '../../hooks'
type GetToken = () => string | undefined | Promise<string | undefined>
type ContextChangeHandler = () => void | Promise<void>
type OptionalWhenDisabled<T> = ({enabled?: true} & T) | ({enabled: false} & Partial<T>)
/**
* Remove the base path from a path string.
* Only strips when path equals basePath or path starts with basePath + '/'.
*/
function removeBasePathFromPath(path: string, basePath: string): string {
const matches =
path.startsWith(basePath + '/') || path === basePath
return matches ? path.slice(basePath.length) || '/' : path
}
/**
* Strip the base path from a path
*
* React Router history re-adds the base path to the path, so we
* remove it here to avoid base path duplication.
*/
function removeBasePathFromLocation<T>(
pathOrLocation: LocationDescriptor<T>,
basePath: string
): LocationDescriptor<T> {
if (!basePath) return pathOrLocation
if (typeof pathOrLocation === 'string') {
return removeBasePathFromPath(pathOrLocation, basePath) as LocationDescriptor<T>
}
const pathname = pathOrLocation.pathname ?? '/'
return {
...pathOrLocation,
pathname: removeBasePathFromPath(pathname, basePath)
}
}
/**
*
* @param enabled - flag to turn on/off Storefront Preview feature. By default, it is set to true.
* This flag only applies if storefront is running in a Runtime Admin iframe.
* @param getToken - A method that returns the access token for the current user
* @param getBasePath - A method that returns the router base path of the app.
*/
export const StorefrontPreview = ({
children,
enabled = true,
getToken,
onContextChange,
getBasePath
}: React.PropsWithChildren<
// Props are only required when Storefront Preview is enabled
OptionalWhenDisabled<{
getToken: GetToken
onContextChange?: ContextChangeHandler
getBasePath?: () => string
}>
>) => {
const history = useHistory()
const isHostTrusted = detectStorefrontPreview()
const apiClients = useCommerceApi()
const {siteId} = useConfig()
useEffect(() => {
if (enabled && isHostTrusted) {
window.STOREFRONT_PREVIEW = {
...window.STOREFRONT_PREVIEW,
getToken,
onContextChange,
siteId,
experimentalUnsafeNavigate: (
path: LocationDescriptor<unknown>,
action: 'push' | 'replace' = 'push',
...args: unknown[]
) => {
const basePath = getBasePath?.() ?? ''
const pathWithoutBase = removeBasePathFromLocation(path, basePath)
history[action](pathWithoutBase, ...args)
}
}
}
}, [enabled, getToken, onContextChange, siteId, getBasePath])
useEffect(() => {
if (enabled && isHostTrusted) {
// In Storefront Preview mode, add cache breaker for all SCAPI's requests.
// Otherwise, it's possible to get stale responses after the Shopper Context is set.
// (i.e. in this case, we optimize for accurate data, rather than performance/caching)
proxyRequests(apiClients, {
apply(target, thisArg, argumentsList) {
argumentsList[0] = {
...argumentsList[0],
parameters: {
...argumentsList[0]?.parameters,
c_cache_breaker: Date.now()
}
}
return target.call(thisArg, ...argumentsList)
}
})
}
}, [apiClients, enabled])
return (
<>
{enabled && isHostTrusted && (
<Helmet>
<script
id="storefront_preview"
src={getClientScript()}
async
type="text/javascript"
></script>
</Helmet>
)}
{children}
</>
)
}
StorefrontPreview.propTypes = {
children: PropTypes.node,
enabled: PropTypes.bool,
// A custom prop type function to only require this prop if enabled is true. Ultimately we would like
// to get to a place where both these props are simply optional and we will provide default implementations.
// This would make the API simpler to use.
getToken: CustomPropTypes.requiredFunctionWhenEnabled,
onContextChange: PropTypes.func,
getBasePath: PropTypes.func
}
export default StorefrontPreview