Skip to content

Commit

Permalink
@W-14016114: Move Storefront Preview Set up to the SDK (#1430)
Browse files Browse the repository at this point in the history
* Move storefront preview set up into the SDK
  • Loading branch information
alexvuong authored Sep 8, 2023
1 parent ab2ebe3 commit 1b72fae
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 12 deletions.
3 changes: 2 additions & 1 deletion packages/commerce-sdk-react/src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
*/

/**
* This list contain domains that can host code in iframe
* This list contains domains that can host code in iframe
*/
export const IFRAME_HOST_ALLOW_LIST = [
'runtime.commercecloud.com',
'runtime-admin-staging.mobify-storefront.com',
'runtime-admin-preview.mobify-storefront.com'
]
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
*/

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():string}} getToken - a STOREFRONT_PREVIEW customised function that fetches token of storefront
*/
export const StorefrontPreview = ({enabled = true, getToken}) => {
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 ? (
<Helmet>
<script
id="storefront_preview"
src={getClientScript()}
async
type="text/javascript"
></script>
</Helmet>
) : null
}

StorefrontPreview.propTypes = {
enabled: PropTypes.bool,
getToken: PropTypes.func
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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)
})
})
12 changes: 12 additions & 0 deletions packages/pwa-kit-react-sdk/src/storefront-preview/index.jsx
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
13 changes: 2 additions & 11 deletions packages/template-retail-react-app/app/components/_app/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
*/

import React, {useState, useEffect} from 'react'
import {Helmet} from 'react-helmet'
import PropTypes from 'prop-types'
import {useHistory, useLocation} from 'react-router-dom'
import StorefrontPreview from '@salesforce/pwa-kit-react-sdk/storefront-preview'
import {getAssetUrl} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils'
import {getAppOrigin} from '@salesforce/pwa-kit-react-sdk/utils/url'
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
Expand Down Expand Up @@ -268,18 +268,8 @@ const App = (props) => {
const path = buildUrl('/account/wishlist')
history.push(path)
}

// Storefront Preview script. Your storefront needs to load this script for Storefront Preview feature
const storefrontPreviewClientScript =
process.env.NODE_ENV === 'development'
? 'http://localhost:4000/mobify/bundle/development/static/storefront-preview.js'
: 'https://runtime.commercecloud.com/cc/b2c/preview/preview.client.js'

return (
<Box className="sf-app" {...styles.container}>
<Helmet>
<script src={storefrontPreviewClientScript} type="text/javascript"></script>
</Helmet>
<IntlProvider
onError={(err) => {
if (!messages) {
Expand All @@ -304,6 +294,7 @@ const App = (props) => {
defaultLocale={DEFAULT_LOCALE}
>
<CurrencyProvider currency={currency}>
<StorefrontPreview />
<Seo>
<meta name="theme-color" content={THEME_COLOR} />
<meta name="apple-mobile-web-app-title" content={DEFAULT_SITE_TITLE} />
Expand Down

0 comments on commit 1b72fae

Please sign in to comment.