Skip to content

Commit 9363706

Browse files
authored
Merge pull request #2194 from SalesforceCommerceCloud/extensibility/default-commerce-provider-store-locator
Add a default commerce api provider to store locator extension
2 parents 7ae5802 + 790c11b commit 9363706

File tree

7 files changed

+264
-5
lines changed

7 files changed

+264
-5
lines changed

packages/extension-chakra-store-locator/README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ If you want to use this without having to install `@chakra-ui` in your project,
3535

3636
### `@salesforce/commerce-sdk-react` Provider
3737

38-
This extension uses the `@salesforce/commerce-sdk-react` package to fetch the store locator data from SCAPI. Your application must use the `CommerceApiProvider` in the React component tree.
38+
This extension uses the `@salesforce/commerce-sdk-react` package to fetch the store locator data from SCAPI. If you provide a `commerceApi` configuration in the extension config, the `CommerceApiProvider` will be added to the React component tree as the default provider. If you already have a `CommerceApiProvider` in your application, do not include the `commerceApi` configuration in the extension config.
3939

4040
## Configurations
4141

@@ -66,7 +66,16 @@ The Store Locator extension is configured via the `mobify.app.extensions` proper
6666
"countryCode": "DE",
6767
"countryName": "Germany"
6868
}
69-
]
69+
],
70+
"commerceApi": {
71+
"proxyPath": "/mobify/proxy/api",
72+
"parameters": {
73+
"shortCode": "8o7m175y",
74+
"clientId": "c9c45bfd-0ed3-4aa2-9971-40f88962b836",
75+
"organizationId": "f_ecom_zzrf_001",
76+
"siteId": "RefArchGlobal"
77+
}
78+
}
7079
}
7180
]
7281
]
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import React from 'react'
8+
import {render, screen} from '@testing-library/react'
9+
import {withOptionalCommerceSdkReactProvider} from './with-optional-commerce-sdk-react-provider'
10+
import PropTypes from 'prop-types'
11+
12+
jest.mock('@salesforce/commerce-sdk-react', () => ({
13+
useCommerceApi: jest.fn(),
14+
// eslint-disable-next-line react/prop-types
15+
CommerceApiProvider: ({children}) => {
16+
return <div data-testid="commerce-provider">{children}</div>
17+
}
18+
}))
19+
20+
jest.mock('@salesforce/pwa-kit-react-sdk/utils/url', () => ({
21+
getAppOrigin: jest.fn(() => 'https://example.com')
22+
}))
23+
24+
describe('withOptionalCommerceSdkReactProvider', () => {
25+
const TestComponent = () => <div>Test Component</div>
26+
const mockConfig = {
27+
commerceApi: {
28+
parameters: {
29+
shortCode: 'test',
30+
clientId: 'test-client',
31+
organizationId: 'test-org',
32+
siteId: 'test-site',
33+
locale: 'en-US',
34+
currency: 'USD'
35+
},
36+
proxyPath: '/api'
37+
}
38+
}
39+
40+
beforeEach(() => {
41+
jest.clearAllMocks()
42+
})
43+
44+
it('wraps component with CommerceApiProvider when no provider exists', () => {
45+
// eslint-disable-next-line @typescript-eslint/no-var-requires
46+
const {useCommerceApi} = require('@salesforce/commerce-sdk-react')
47+
useCommerceApi.mockImplementation(() => {
48+
throw new Error('No provider')
49+
})
50+
51+
const WrappedComponent = withOptionalCommerceSdkReactProvider(TestComponent, mockConfig)
52+
const {container} = render(<WrappedComponent />)
53+
54+
expect(screen.getByTestId('commerce-provider')).toBeTruthy()
55+
expect(screen.getByText('Test Component')).toBeTruthy()
56+
})
57+
58+
it('does not wrap component when provider already exists', () => {
59+
// eslint-disable-next-line @typescript-eslint/no-var-requires
60+
const {useCommerceApi} = require('@salesforce/commerce-sdk-react')
61+
useCommerceApi.mockReturnValue({ShopperProducts: {}, ShopperBaskets: {}})
62+
63+
const WrappedComponent = withOptionalCommerceSdkReactProvider(TestComponent, mockConfig)
64+
const {container} = render(<WrappedComponent />)
65+
66+
expect(container.querySelector('[data-testid="commerce-provider"]')).toBeNull()
67+
expect(screen.getByText('Test Component')).toBeTruthy()
68+
})
69+
70+
it('passes props to wrapped component', () => {
71+
const TestComponentWithProps = ({testProp}) => <div>{testProp}</div>
72+
TestComponentWithProps.propTypes = {
73+
testProp: PropTypes.string
74+
}
75+
76+
// eslint-disable-next-line @typescript-eslint/no-var-requires
77+
const {useCommerceApi} = require('@salesforce/commerce-sdk-react')
78+
useCommerceApi.mockImplementation(() => {
79+
throw new Error('No provider')
80+
})
81+
82+
const WrappedComponent = withOptionalCommerceSdkReactProvider(
83+
TestComponentWithProps,
84+
mockConfig
85+
)
86+
render(<WrappedComponent testProp="test value" />)
87+
88+
expect(screen.getByText('test value')).toBeTruthy()
89+
})
90+
91+
it('renders wrapped component without provider when config is missing', () => {
92+
// eslint-disable-next-line @typescript-eslint/no-var-requires
93+
const {useCommerceApi} = require('@salesforce/commerce-sdk-react')
94+
useCommerceApi.mockImplementation(() => {
95+
throw new Error('No provider')
96+
})
97+
98+
const invalidConfig = {}
99+
const WrappedComponent = withOptionalCommerceSdkReactProvider(TestComponent, invalidConfig)
100+
const {container} = render(<WrappedComponent />)
101+
102+
expect(container.querySelector('[data-testid="commerce-provider"]')).toBeNull()
103+
expect(screen.getByText('Test Component')).toBeTruthy()
104+
})
105+
})
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) 2025, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import React from 'react'
9+
import {getAppOrigin} from '@salesforce/pwa-kit-react-sdk/utils/url'
10+
import {CommerceApiProvider, useCommerceApi} from '@salesforce/commerce-sdk-react'
11+
import {UserConfig} from '../types/config'
12+
import {logger} from '../logger'
13+
14+
/**
15+
* Checks if the CommerceApiProvider is already installed in the component tree.
16+
* @returns boolean, true if the CommerceApiProvider is installed, false otherwise.
17+
*/
18+
const useHasCommerceApiProvider = () => {
19+
let hasProvider = false
20+
21+
try {
22+
const api = useCommerceApi()
23+
24+
// the api object is an object with a bunch of api clients like ShopperProduct, ShopperOrder, etc.
25+
// if the object is empty, then the CommerceApiProvider is not installed
26+
if (Object.keys(api).length > 0) {
27+
hasProvider = true
28+
}
29+
} catch (_) {
30+
hasProvider = false
31+
}
32+
33+
return hasProvider
34+
}
35+
36+
type WithOptionalCommerceSdkReactProvider = React.ComponentPropsWithoutRef<any>
37+
38+
/**
39+
* Higher-order component that conditionally installs the CommerceApiProvider if the config is provided.
40+
*
41+
* @param WrappedComponent - The component to be optionally wrapped with CommerceApiProvider.
42+
* @param config - The configuration object for the CommerceApiProvider.
43+
* @returns A component that wraps the given component with CommerceApiProvider if it is not already present in the component tree.
44+
*/
45+
export const withOptionalCommerceSdkReactProvider = <P extends object>(
46+
WrappedComponent: React.ComponentType<P>,
47+
config: UserConfig
48+
) => {
49+
const HOC: React.FC<P> = (props: WithOptionalCommerceSdkReactProvider) => {
50+
if (useHasCommerceApiProvider()) {
51+
return <WrappedComponent {...(props as P)} />
52+
}
53+
if (!config.commerceApi || !config.commerceApi?.parameters) {
54+
logger.error(
55+
'CommerceApiProvider is not installed and no commerceApi config is provided, this extension may not work as expected.'
56+
)
57+
return <WrappedComponent {...(props as P)} />
58+
}
59+
const appOrigin = getAppOrigin()
60+
return (
61+
<CommerceApiProvider
62+
shortCode={config.commerceApi.parameters.shortCode}
63+
clientId={config.commerceApi.parameters.clientId}
64+
organizationId={config.commerceApi.parameters.organizationId}
65+
siteId={config.commerceApi.parameters.siteId}
66+
locale={config.commerceApi.parameters.locale}
67+
currency={config.commerceApi.parameters.currency}
68+
redirectURI={`${appOrigin}/callback`}
69+
proxy={`${appOrigin}${config.commerceApi.proxyPath}`}
70+
>
71+
<WrappedComponent {...(props as P)} />
72+
</CommerceApiProvider>
73+
)
74+
}
75+
76+
return HOC
77+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import createLogger from '@salesforce/pwa-kit-runtime/utils/logger-factory'
8+
9+
export const logger = createLogger({packageName: 'extension-chakra-store-locator'})

packages/extension-chakra-store-locator/src/setup-app.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ import {RouteProps} from 'react-router-dom'
1111

1212
// Platform Imports
1313
import {ApplicationExtension} from '@salesforce/pwa-kit-extension-sdk/react'
14+
import {applyHOCs} from '@salesforce/pwa-kit-extension-sdk/react/utils'
1415

1516
// Local Imports
1617
import {withOptionalChakra} from './components/with-optional-chakra-provider'
18+
import {withOptionalCommerceSdkReactProvider} from './components/with-optional-commerce-sdk-react-provider'
1719
import {withStoreLocator} from './components/with-store-locator'
1820
import {Config} from './types'
1921

2022
import StoreLocatorPage from './pages/store-locator'
23+
import {logger} from './logger'
2124
import extensionMeta from '../extension-meta.json'
2225

2326
class StoreLocatorExtension extends ApplicationExtension<Config> {
@@ -29,13 +32,19 @@ class StoreLocatorExtension extends ApplicationExtension<Config> {
2932
const config = this.getConfig()
3033

3134
if (!config.supportedCountries || config.supportedCountries.length === 0) {
32-
// TODO: use our logger
33-
console.warn(
35+
logger.error(
3436
'[extension-chakra-store-locator] Missing supportedCountries, this extension will not work.'
3537
)
3638
}
3739

38-
return withStoreLocator(withOptionalChakra(App), config)
40+
const HOCs = [
41+
(component: React.ComponentType<any>) => withStoreLocator(component, config),
42+
(component: React.ComponentType<any>) =>
43+
withOptionalCommerceSdkReactProvider(component, config),
44+
(component: React.ComponentType<any>) => withOptionalChakra(component)
45+
]
46+
47+
return applyHOCs(App, HOCs)
3948
}
4049

4150
extendRoutes(routes: RouteProps[]): RouteProps[] {

packages/extension-chakra-store-locator/src/types/config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ export interface UserConfig extends ApplicationExtensionConfig {
2121
countryCode: string
2222
countryName: string
2323
}>
24+
commerceApi?: {
25+
proxyPath: string
26+
parameters: {
27+
shortCode: string
28+
clientId: string
29+
organizationId: string
30+
siteId: string
31+
locale: string
32+
currency: string
33+
}
34+
}
2435
}
2536

2637
/**

packages/template-typescript-minimal/package.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@emotion/react": "^11.13.3",
2323
"@emotion/styled": "^11.13.0",
2424
"@loadable/component": "^5.15.3",
25+
"@salesforce/extension-chakra-store-locator": "^0.1.0-extensibility-preview.3",
2526
"@salesforce/pwa-kit-dev": "4.0.0-extensibility-preview.3",
2627
"@salesforce/pwa-kit-extension-sdk": "4.0.0-extensibility-preview.3",
2728
"@salesforce/pwa-kit-react-sdk": "4.0.0-extensibility-preview.3",
@@ -44,6 +45,44 @@
4445
"npm": "^8.0.0 || ^9.0.0 || ^10.0.0"
4546
},
4647
"mobify": {
48+
"app": {
49+
"extensions": [
50+
[
51+
"@salesforce/extension-chakra-store-locator",
52+
{
53+
"enabled": true,
54+
"path": "/store-locator",
55+
"radius": 100,
56+
"radiusUnit": "km",
57+
"defaultPageSize": 10,
58+
"defaultPostalCode": "10178",
59+
"defaultCountry": "Germany",
60+
"defaultCountryCode": "DE",
61+
"supportedCountries": [
62+
{
63+
"countryCode": "US",
64+
"countryName": "United States"
65+
},
66+
{
67+
"countryCode": "DE",
68+
"countryName": "Germany"
69+
}
70+
],
71+
"commerceApi": {
72+
"proxyPath": "/mobify/proxy/api",
73+
"parameters": {
74+
"shortCode": "8o7m175y",
75+
"clientId": "c9c45bfd-0ed3-4aa2-9971-40f88962b836",
76+
"organizationId": "f_ecom_zzrf_001",
77+
"siteId": "RefArchGlobal",
78+
"locale": "en-GB",
79+
"currency": "USD"
80+
}
81+
}
82+
}
83+
]
84+
]
85+
},
4786
"ssrEnabled": true,
4887
"ssrOnly": [
4988
"ssr.js",

0 commit comments

Comments
 (0)