-
Notifications
You must be signed in to change notification settings - Fork 145
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
@W-14016114: Move Storefront Preview Set up to the SDK #1430
Changes from 32 commits
b0fd556
75093f2
2bab299
06068e9
2dad129
64f3c5b
a433631
e5d51d1
d03135a
7fc2caf
8d35e9b
c02ccbc
792af53
857341e
588c2bc
2f15a10
7342be1
db269a6
1c19db5
9cc11b8
ec97c7f
8b6925e
7a6d7b3
6cf285b
a8d46e1
2764c0c
38cc91c
e906c4a
14f63f6
1f270b4
3b213f7
134fe93
f19dc6b
0095b3e
d497d9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/* | ||
* 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 {detectStorefrontPreview, getClientScript} from './utils' | ||
|
||
/** | ||
* | ||
* @param {boolean} enabled - flag to turn on/off Storefront Preview feature | ||
* @param {function} getToken - a STOREFRONT_PREVIEW customised function that fetches token of storefront | ||
*/ | ||
export const StorefrontPreview = ({enabled = true, getToken}) => { | ||
kevinxh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let isHostTrusted | ||
useEffect(() => { | ||
if (enabled && isHostTrusted) { | ||
window.STOREFRONT_PREVIEW = { | ||
...window.STOREFRONT_PREVIEW, | ||
getToken | ||
} | ||
} | ||
}, [enabled, getToken]) | ||
if (!enabled) { | ||
return null | ||
} | ||
// We only want to run this function when enabled is on | ||
isHostTrusted = detectStorefrontPreview() | ||
return ( | ||
isHostTrusted && ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The component returns a boolean, do you mean to return a |
||
<Helmet> | ||
<script src={getClientScript()} async type="text/javascript"></script> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should the script be conditionally rendered if It seems to me the script is always loaded even when outside the context of storefront preview iframe? |
||
</Helmet> | ||
) | ||
) | ||
} | ||
|
||
StorefrontPreview.propTypes = { | ||
enabled: PropTypes.bool, | ||
getToken: PropTypes.func | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* | ||
alexvuong marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* 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 from 'react' | ||
import {render, waitFor} from '@testing-library/react' | ||
import {StorefrontPreview} from './index' | ||
import {detectStorefrontPreview} from './utils' | ||
import {Helmet} from 'react-helmet' | ||
|
||
jest.mock('./utils', () => { | ||
const origin = jest.requireActual('./utils') | ||
return { | ||
...origin, | ||
detectStorefrontPreview: jest.fn() | ||
} | ||
}) | ||
describe('Storefront Preview Component', function () { | ||
const oldWindow = window | ||
|
||
beforeEach(() => { | ||
// eslint-disable-next-line | ||
window = {...oldWindow} | ||
}) | ||
|
||
afterEach(() => { | ||
// eslint-disable-next-line | ||
window = oldWindow | ||
}) | ||
test('not renders nothing when enabled is off', async () => { | ||
render(<StorefrontPreview enabled={false} />) | ||
const helmet = Helmet.peek() | ||
await waitFor(() => { | ||
expect(helmet).toBeUndefined() | ||
}) | ||
}) | ||
test('renders script tag when enabled is on', async () => { | ||
detectStorefrontPreview.mockReturnValue(true) | ||
|
||
render(<StorefrontPreview enabled={true} />) | ||
// this will return all the markup assigned to helmet | ||
// which will get rendered inside head. | ||
const helmet = Helmet.peek() | ||
await waitFor(() => { | ||
expect(helmet.scriptTags[0].src).toBe( | ||
'https://runtime.commercecloud.com/cc/b2c/preview/preview.client.js' | ||
) | ||
expect(helmet.scriptTags[0].async).toBe(true) | ||
expect(helmet.scriptTags[0].type).toBe('text/javascript') | ||
}) | ||
}) | ||
|
||
test('renders script tag when window.STOREFRONT_PREVIEW.enabled is on', async () => { | ||
delete window.STOREFRONT_PREVIEW | ||
window.STOREFRONT_PREVIEW = {} | ||
window.STOREFRONT_PREVIEW.enabled = true | ||
detectStorefrontPreview.mockReturnValue(true) | ||
|
||
render(<StorefrontPreview />) | ||
// this will return all the markup assigned to helmet | ||
// which will get rendered inside head. | ||
const helmet = Helmet.peek() | ||
await waitFor(() => { | ||
expect(helmet.scriptTags[0].src).toBe( | ||
'https://runtime.commercecloud.com/cc/b2c/preview/preview.client.js' | ||
) | ||
expect(helmet.scriptTags[0].async).toBe(true) | ||
expect(helmet.scriptTags[0].type).toBe('text/javascript') | ||
}) | ||
}) | ||
|
||
test('getToken is defined in window.STOREFRONT_PREVIEW when it is defined', async () => { | ||
window.STOREFRONT_PREVIEW = {} | ||
window.STOREFRONT_PREVIEW.enabled = true | ||
detectStorefrontPreview.mockReturnValue(true) | ||
|
||
render(<StorefrontPreview getToken={() => 'my-token'} />) | ||
expect(window.STOREFRONT_PREVIEW.getToken).toBeDefined() | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* 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 | ||
*/ | ||
|
||
/** Origins that are allowed to run Storefront Preview. */ | ||
const TRUSTED_ORIGINS = [ | ||
'https://runtime.commercecloud.com', | ||
'https://runtime-admin-staging.mobify-storefront.com', | ||
'https://runtime-admin-preview.mobify-storefront.com' | ||
] | ||
|
||
/** Origin used for local Runtime Admin. */ | ||
const DEVELOPMENT_ORIGIN = 'http://localhost:4000' | ||
|
||
/** Detects whether the storefront is running in an iframe. */ | ||
const detectInIframe = () => typeof window !== 'undefined' && window.parent !== window.self | ||
|
||
/** Gets the parent origin when running in an iframe. */ | ||
export const getParentOrigin = () => { | ||
if (detectInIframe()) { | ||
if (window.location.ancestorOrigins) return window.location.ancestorOrigins[0] | ||
// ancestorOrigins does not exist in Firefox, so we use referrer as a fallback | ||
if (document.referrer) return new URL(document.referrer).origin | ||
} | ||
} | ||
|
||
const isParentOriginTrusted = (parentOrigin) => { | ||
return window.location.hostname === 'localhost' | ||
? parentOrigin === DEVELOPMENT_ORIGIN // Development | ||
: TRUSTED_ORIGINS.includes(parentOrigin) // Production | ||
} | ||
|
||
/** Detects whether the storefront is running in an iframe as part of Storefront Preview. */ | ||
export const detectStorefrontPreview = () => { | ||
const parentOrigin = getParentOrigin() | ||
return Boolean(parentOrigin) && isParentOriginTrusted(parentOrigin) | ||
} | ||
|
||
/** Returns the URL to load the Storefront Preview client script from the parent origin. */ | ||
export const getClientScript = () => { | ||
const parentOrigin = getParentOrigin() ?? 'https://runtime.commercecloud.com' | ||
return parentOrigin === DEVELOPMENT_ORIGIN | ||
? `${parentOrigin}/mobify/bundle/development/static/storefront-preview.js` | ||
: `${parentOrigin}/cc/b2c/preview/preview.client.js` | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
* 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 {getClientScript, getParentOrigin, detectStorefrontPreview} from './utils' | ||
|
||
describe('getClientScript', function () { | ||
const oldWindow = window | ||
|
||
beforeEach(() => { | ||
// eslint-disable-next-line | ||
window = {...oldWindow} | ||
}) | ||
|
||
afterEach(() => { | ||
// eslint-disable-next-line | ||
window = oldWindow | ||
}) | ||
test('returns client script src with prod origin', () => { | ||
const src = getClientScript() | ||
expect(src).toBe('https://runtime.commercecloud.com/cc/b2c/preview/preview.client.js') | ||
}) | ||
|
||
test('returns client script src with localhost origin', () => { | ||
// Delete the real properties from window so we can mock them | ||
delete window.parent | ||
|
||
window.parent = {} | ||
window.location.ancestorOrigins = ['http://localhost:4000'] | ||
const src = getClientScript() | ||
|
||
expect(src).toBe( | ||
'http://localhost:4000/mobify/bundle/development/static/storefront-preview.js' | ||
) | ||
}) | ||
}) | ||
|
||
describe('getParentOrigin', function () { | ||
test('returns origin from ancestorOrigins', () => { | ||
// Delete the real properties from window so we can mock them | ||
delete window.parent | ||
|
||
window.parent = {} | ||
const localHostOrigin = 'http://localhost:4000' | ||
window.location.ancestorOrigins = [localHostOrigin] | ||
const origin = getParentOrigin() | ||
|
||
expect(origin).toBe(localHostOrigin) | ||
}) | ||
test('returns origin from document.referrer', () => { | ||
// Delete the real properties from window so we can mock them | ||
delete window.parent | ||
|
||
window.parent = {} | ||
delete window.location.ancestorOrigins | ||
const localHostOrigin = 'http://localhost:4000' | ||
jest.spyOn(document, 'referrer', 'get').mockReturnValue(localHostOrigin) | ||
const origin = getParentOrigin() | ||
|
||
expect(origin).toBe(localHostOrigin) | ||
}) | ||
}) | ||
describe('detectStorefrontPreview', function () { | ||
test('returns true for trusted origin', () => { | ||
// Delete the real properties from window so we can mock them | ||
delete window.parent | ||
delete window.location | ||
|
||
window.parent = {} | ||
window.location = {} | ||
window.location.hostname = 'localhost' | ||
const localHostOrigin = 'http://localhost:4000' | ||
window.location.ancestorOrigins = [localHostOrigin] | ||
expect(detectStorefrontPreview()).toBe(true) | ||
}) | ||
test('returns false for non-trusted origin', () => { | ||
// Delete the real properties from window so we can mock them | ||
delete window.parent | ||
delete window.location | ||
|
||
window.parent = {} | ||
window.location = {} | ||
window.location.hostname = 'localhost' | ||
const localHostOrigin = 'http://localhost:4000' | ||
window.location.ancestorOrigins = [localHostOrigin] | ||
expect(detectStorefrontPreview()).toBe(true) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/* | ||
* 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 | ||
*/ | ||
/** | ||
* Create alias import path for StorefrontPreview component | ||
* to indicate this is a first-class feature | ||
*/ | ||
import {StorefrontPreview} from '../ssr/universal/components/storefront-preview' | ||
export default StorefrontPreview |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.