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

@W-14016114: Move Storefront Preview Set up to the SDK #1430

Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b0fd556
Update Storefront Preview client script URL.
wjhsf Aug 28, 2023
75093f2
Create Storefront Preview component.
wjhsf Aug 30, 2023
2bab299
Temp fixes to see changes.
wjhsf Aug 30, 2023
06068e9
Revert "Temp fixes to see changes."
wjhsf Aug 30, 2023
2dad129
Change Storefront Preview component to hook in react sdk.
wjhsf Aug 31, 2023
64f3c5b
Use origins, not hostnames.
wjhsf Aug 31, 2023
a433631
Affordances for local development.
wjhsf Aug 31, 2023
e5d51d1
Make cleanup fast and loose.
wjhsf Aug 31, 2023
d03135a
Allow Runtime Admin staging environment to host iframes.
wjhsf Aug 31, 2023
7fc2caf
Use parent origin when loading client script.
wjhsf Aug 31, 2023
8d35e9b
Load script in effect instead of component.
wjhsf Aug 31, 2023
c02ccbc
add preview script based on origin
alexvuong Sep 2, 2023
792af53
Merge branch 'feature/v3-storefront-preview' into preview/add-preview…
alexvuong Sep 5, 2023
857341e
create storefront preview Provider
alexvuong Sep 5, 2023
588c2bc
fix naming
alexvuong Sep 5, 2023
2f15a10
use normal component instead of a provider
alexvuong Sep 6, 2023
7342be1
add preview storefront component in app in retail app instead of conf…
alexvuong Sep 6, 2023
db269a6
minor tweak
alexvuong Sep 6, 2023
1c19db5
minor tweak
alexvuong Sep 6, 2023
9cc11b8
set up STOREFRONT_PREVIEW in window object
alexvuong Sep 6, 2023
ec97c7f
linting
alexvuong Sep 6, 2023
8b6925e
clean up
alexvuong Sep 7, 2023
7a6d7b3
PR feedback
alexvuong Sep 7, 2023
6cf285b
add tests
alexvuong Sep 7, 2023
a8d46e1
add tests
alexvuong Sep 7, 2023
2764c0c
add tests
alexvuong Sep 7, 2023
38cc91c
linting
alexvuong Sep 7, 2023
e906c4a
add comment
alexvuong Sep 7, 2023
14f63f6
create path alias
alexvuong Sep 7, 2023
1f270b4
add comment
alexvuong Sep 7, 2023
3b213f7
remove using MRT env detection
alexvuong Sep 8, 2023
134fe93
PR feedback
alexvuong Sep 8, 2023
f19dc6b
PR feedback
alexvuong Sep 8, 2023
0095b3e
PR feedback
alexvuong Sep 8, 2023
d497d9d
PR feedback
alexvuong Sep 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'
]
5 changes: 5 additions & 0 deletions packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ const renderApp = (args) => {
__CONFIG__: config,
__PRELOADED_STATE__: appState,
__ERROR__: error,
// process.env.STOREFRONT_PREVIEW is an MRT env, by default it will be turned off/undefined, customer will need to set it on
// for env that they want to use Preview
STOREFRONT_PREVIEW: {
enabled: process.env.STOREFRONT_PREVIEW
},
// `window.Progressive` has a long history at Mobify and some
// client-side code depends on it. Maintain its name out of tradition.
Progressive: getWindowProgressive(req, res)
Expand Down
alexvuong marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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'

export const StorefrontPreview = ({
enabled = typeof window !== 'undefined' ? Boolean(window.STOREFRONT_PREVIEW?.enabled) : false,
getToken
}) => {
let isHostTrusted
useEffect(() => {
if (enabled && isHostTrusted) {
window.STOREFRONT_PREVIEW = {
...window.STOREFRONT_PREVIEW,
getToken
}
}
}, [enabled, getToken])
if (!enabled) {
return null
}
isHostTrusted = detectStorefrontPreview()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to run this function unless enabled is true

return (
<>
<Helmet>
<script src={getClientScript()} async type="text/javascript"></script>
</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, screen, 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(() => {
window = {...oldWindow}
})

afterEach(() => {
window = oldWindow
})
test('not renders nothing when enabled is off', async () => {
render(<StorefrontPreview enabled={false} />)
const helmet = Helmet.peek()
await waitFor(() => {
expect(helmet).toEqual(undefined)
})
})
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).toEqual(
'https://runtime.commercecloud.com/cc/b2c/preview/preview.client.js'
)
expect(helmet.scriptTags[0].async).toEqual(true)
expect(helmet.scriptTags[0].type).toEqual('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).toEqual(
'https://runtime.commercecloud.com/cc/b2c/preview/preview.client.js'
)
expect(helmet.scriptTags[0].async).toEqual(true)
expect(helmet.scriptTags[0].type).toEqual('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()
})
})

describe('Test', function () {})
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,89 @@
/*
* 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(() => {
window = {...oldWindow}
})

afterEach(() => {
window = oldWindow
})
test('returns client script src with prod origin', () => {
const src = getClientScript()
expect(src).toEqual('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).toEqual(
'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)
})
})
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/ssr/universal/components/storefront-preview'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside: To emphasize Storefront Preview as a first-class feature, maybe we should create an alias import for the file: @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 />
alexvuong marked this conversation as resolved.
Show resolved Hide resolved
wjhsf marked this conversation as resolved.
Show resolved Hide resolved
<Seo>
<meta name="theme-color" content={THEME_COLOR} />
<meta name="apple-mobile-web-app-title" content={DEFAULT_SITE_TITLE} />
Expand Down