Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Storefront Preview 🔍 ] Add "onContextChange" Property to StorefrontPreview Component #1527

Merged
merged 11 commits into from
Nov 17, 2023
2 changes: 2 additions & 0 deletions packages/commerce-sdk-react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## v1.2.0-dev (Nov 03, 2023)
- Add StorefrontPreview component 'onContextChange' property. [#1527](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1527)

## v1.1.0 (Nov 03, 2023)

- Add StorefrontPreview component [#1508](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1508)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ describe('Storefront Preview Component', function () {
expect(window.STOREFRONT_PREVIEW.getToken).toBeDefined()
})

test('onContextChange is defined in window.STOREFRONT_PREVIEW when it is defined', () => {
window.STOREFRONT_PREVIEW = {}
;(detectStorefrontPreview as jest.Mock).mockReturnValue(true)

render(<StorefrontPreview enabled={true} getToken={() => 'my-token'} onContextChange={() => undefined} />)
expect(window.STOREFRONT_PREVIEW.onContextChange).toBeDefined()
})

test('experimental unsafe props are defined', () => {
expect(window.STOREFRONT_PREVIEW.experimentalUnsafeNavigate).toBeDefined()
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
import React, {useEffect} from 'react'
import PropTypes from 'prop-types'
import {Helmet} from 'react-helmet'
import {detectStorefrontPreview, getClientScript} from './utils'
import {CustomPropTypes, detectStorefrontPreview, getClientScript, noop} from './utils'
import {useHistory} from 'react-router-dom'
import type {LocationDescriptor} from 'history'

type GetToken = () => string | undefined | Promise<string | undefined>
type ContextChangeHandler = () => void | Promise<void>

/**
*
Expand All @@ -23,14 +24,15 @@ type GetToken = () => string | undefined | Promise<string | undefined>
export const StorefrontPreview = ({
children,
enabled = true,
getToken
getToken,
onContextChange
}: React.PropsWithChildren<
// getToken is required unless enabled is false
{enabled?: true; getToken: GetToken} | {enabled: false; getToken?: GetToken}
// getToken and onContextChange are only required when enabled = true.
{enabled?: true; getToken: GetToken; onContextChange?: ContextChangeHandler;} | {enabled: false; getToken?: GetToken; onContextChange?: ContextChangeHandler;}
bendvc marked this conversation as resolved.
Show resolved Hide resolved
>) => {
const history = useHistory()
const isHostTrusted = detectStorefrontPreview()

useEffect(() => {
if (enabled && isHostTrusted) {
window.STOREFRONT_PREVIEW = {
Expand All @@ -42,10 +44,11 @@ export const StorefrontPreview = ({
...args: unknown[]
) => {
history[action](path, ...args)
}
},
onContextChange: onContextChange || noop
bendvc marked this conversation as resolved.
Show resolved Hide resolved
}
}
}, [enabled, getToken])
}, [enabled, getToken, onContextChange])

return (
<>
Expand All @@ -67,19 +70,11 @@ export const StorefrontPreview = ({
StorefrontPreview.propTypes = {
children: PropTypes.node,
enabled: PropTypes.bool,
// a custom prop type function to only require this prop if enabled is true.
getToken: function (props: any, propName: any, componentName: any) {
if (
props['enabled'] === true &&
(props[propName] === undefined || typeof props[propName] !== 'function')
) {
return new Error(
`${String(propName)} is a required function for ${String(
componentName
)} when enabled is true`
)
}
}
// 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: CustomPropTypes.requiredFunctionWhenEnabled
bendvc marked this conversation as resolved.
Show resolved Hide resolved
}

export default StorefrontPreview
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,36 @@ export const getClientScript = () => {
? `${parentOrigin}/mobify/bundle/development/static/storefront-preview.js`
: `${parentOrigin}/cc/b2c/preview/preview.client.js`
}

/**
* Do nothing!
*
* @returns undefined
*/
export const noop = () => undefined


// Custom Prop Types.
export const CustomPropTypes = {
/**
* This custom PropType ensures that the prop is only required when the known prop
* "enabled" is set to "true".
*
* @param props
* @param propName
* @param componentName
* @returns
*/
requiredFunctionWhenEnabled: (props: any, propName: any, componentName: any) => {
if (
props['enabled'] === true &&
(props[propName] === undefined || typeof props[propName] !== 'function')
) {
return new Error(
`${String(propName)} is a required function for ${String(
componentName
)} when enabled is true`
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,37 @@ const App = (props) => {
const path = buildUrl('/account/wishlist')
history.push(path)
}

// START - Demo "onContextChange" handler.
// TODO: Remove before merging PR
const fakeRequest = (options = {}) => {
const failurePercentage = options?.failurePercentage || 0 // 0 --> 1
const maxDuration = options?.maxDuration || 15000
const duration = Math.floor(Math.random() * maxDuration)

return new Promise((resolve, reject) =>
setTimeout(() => {
if (Math.random() < failurePercentage) {
reject(new Error('A random network error occured!'))
} else {
resolve()
}
}, duration)
)
}

const contextChangeHandler = async (context) => {
// NOTE: We know that the timeout error will occur after 10 seconds, so having a max wait
// of 15 seconds means there is a random chance we will wait longer than the 10 second timeout.
console.log(`STOREFRONT: Simulating network request on questionable network.`, context)
await fakeRequest({maxDuration: 15000, failurePercentage: 0.4}) // 40% of the time we will fail!
console.log('STOREFRONT: Finished custom context handler.')
}
// END

return (
<Box className="sf-app" {...styles.container}>
<StorefrontPreview getToken={getTokenWhenReady}>
<StorefrontPreview getToken={getTokenWhenReady} onContextChange={contextChangeHandler}>
<IntlProvider
onError={(err) => {
if (!messages) {
Expand Down