-
Notifications
You must be signed in to change notification settings - Fork 212
Add a default commerce api provider to store locator extension #2194
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
Changes from all commits
7a79859
02349dd
8320948
fc5b271
7173909
d69e3a2
2020b94
cb4964f
5027ecc
532c84c
0494743
c3f1d67
f240cdc
790c11b
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,105 @@ | ||
| /* | ||
| * Copyright (c) 2025, 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} from '@testing-library/react' | ||
| import {withOptionalCommerceSdkReactProvider} from './with-optional-commerce-sdk-react-provider' | ||
| import PropTypes from 'prop-types' | ||
|
|
||
| jest.mock('@salesforce/commerce-sdk-react', () => ({ | ||
| useCommerceApi: jest.fn(), | ||
| // eslint-disable-next-line react/prop-types | ||
| CommerceApiProvider: ({children}) => { | ||
| return <div data-testid="commerce-provider">{children}</div> | ||
| } | ||
| })) | ||
|
|
||
| jest.mock('@salesforce/pwa-kit-react-sdk/utils/url', () => ({ | ||
| getAppOrigin: jest.fn(() => 'https://example.com') | ||
| })) | ||
|
|
||
| describe('withOptionalCommerceSdkReactProvider', () => { | ||
| const TestComponent = () => <div>Test Component</div> | ||
| const mockConfig = { | ||
| commerceApi: { | ||
| parameters: { | ||
| shortCode: 'test', | ||
| clientId: 'test-client', | ||
| organizationId: 'test-org', | ||
| siteId: 'test-site', | ||
| locale: 'en-US', | ||
| currency: 'USD' | ||
| }, | ||
| proxyPath: '/api' | ||
| } | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks() | ||
| }) | ||
|
|
||
| it('wraps component with CommerceApiProvider when no provider exists', () => { | ||
| // eslint-disable-next-line @typescript-eslint/no-var-requires | ||
| const {useCommerceApi} = require('@salesforce/commerce-sdk-react') | ||
| useCommerceApi.mockImplementation(() => { | ||
| throw new Error('No provider') | ||
| }) | ||
|
|
||
| const WrappedComponent = withOptionalCommerceSdkReactProvider(TestComponent, mockConfig) | ||
| const {container} = render(<WrappedComponent />) | ||
|
|
||
| expect(screen.getByTestId('commerce-provider')).toBeTruthy() | ||
| expect(screen.getByText('Test Component')).toBeTruthy() | ||
| }) | ||
|
|
||
| it('does not wrap component when provider already exists', () => { | ||
| // eslint-disable-next-line @typescript-eslint/no-var-requires | ||
| const {useCommerceApi} = require('@salesforce/commerce-sdk-react') | ||
| useCommerceApi.mockReturnValue({ShopperProducts: {}, ShopperBaskets: {}}) | ||
|
|
||
| const WrappedComponent = withOptionalCommerceSdkReactProvider(TestComponent, mockConfig) | ||
| const {container} = render(<WrappedComponent />) | ||
|
|
||
| expect(container.querySelector('[data-testid="commerce-provider"]')).toBeNull() | ||
| expect(screen.getByText('Test Component')).toBeTruthy() | ||
| }) | ||
|
|
||
| it('passes props to wrapped component', () => { | ||
| const TestComponentWithProps = ({testProp}) => <div>{testProp}</div> | ||
| TestComponentWithProps.propTypes = { | ||
| testProp: PropTypes.string | ||
| } | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-var-requires | ||
| const {useCommerceApi} = require('@salesforce/commerce-sdk-react') | ||
| useCommerceApi.mockImplementation(() => { | ||
| throw new Error('No provider') | ||
| }) | ||
|
|
||
| const WrappedComponent = withOptionalCommerceSdkReactProvider( | ||
| TestComponentWithProps, | ||
| mockConfig | ||
| ) | ||
| render(<WrappedComponent testProp="test value" />) | ||
|
|
||
| expect(screen.getByText('test value')).toBeTruthy() | ||
| }) | ||
|
|
||
| it('renders wrapped component without provider when config is missing', () => { | ||
| // eslint-disable-next-line @typescript-eslint/no-var-requires | ||
| const {useCommerceApi} = require('@salesforce/commerce-sdk-react') | ||
| useCommerceApi.mockImplementation(() => { | ||
| throw new Error('No provider') | ||
| }) | ||
|
|
||
| const invalidConfig = {} | ||
| const WrappedComponent = withOptionalCommerceSdkReactProvider(TestComponent, invalidConfig) | ||
| const {container} = render(<WrappedComponent />) | ||
|
|
||
| expect(container.querySelector('[data-testid="commerce-provider"]')).toBeNull() | ||
| expect(screen.getByText('Test Component')).toBeTruthy() | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| /* | ||
| * Copyright (c) 2025, salesforce.com, 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 {getAppOrigin} from '@salesforce/pwa-kit-react-sdk/utils/url' | ||
| import {CommerceApiProvider, useCommerceApi} from '@salesforce/commerce-sdk-react' | ||
| import {UserConfig} from '../types/config' | ||
| import {logger} from '../logger' | ||
|
|
||
| /** | ||
| * Checks if the CommerceApiProvider is already installed in the component tree. | ||
| * @returns boolean, true if the CommerceApiProvider is installed, false otherwise. | ||
| */ | ||
| const useHasCommerceApiProvider = () => { | ||
| let hasProvider = false | ||
|
|
||
| try { | ||
| const api = useCommerceApi() | ||
|
|
||
| // the api object is an object with a bunch of api clients like ShopperProduct, ShopperOrder, etc. | ||
| // if the object is empty, then the CommerceApiProvider is not installed | ||
| if (Object.keys(api).length > 0) { | ||
| hasProvider = true | ||
| } | ||
| } catch (_) { | ||
| hasProvider = false | ||
| } | ||
|
|
||
| return hasProvider | ||
| } | ||
|
|
||
| type WithOptionalCommerceSdkReactProvider = React.ComponentPropsWithoutRef<any> | ||
|
|
||
| /** | ||
| * Higher-order component that conditionally installs the CommerceApiProvider if the config is provided. | ||
| * | ||
| * @param WrappedComponent - The component to be optionally wrapped with CommerceApiProvider. | ||
| * @param config - The configuration object for the CommerceApiProvider. | ||
| * @returns A component that wraps the given component with CommerceApiProvider if it is not already present in the component tree. | ||
| */ | ||
| export const withOptionalCommerceSdkReactProvider = <P extends object>( | ||
|
Contributor
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. Not required, but if this is going to be a pattern that we intend on suggesting extension developers use, then it might be worth while to create a utility that will take a provider component and a hook that is used to determine if there is a preexisting provider already and spit out an optional provider. Something like this: Where the first arg is the component that you want to optionally wrap and assertionHook option is a hook that will best tested to see if it returns a truthy value. |
||
| WrappedComponent: React.ComponentType<P>, | ||
| config: UserConfig | ||
| ) => { | ||
| const HOC: React.FC<P> = (props: WithOptionalCommerceSdkReactProvider) => { | ||
| if (useHasCommerceApiProvider()) { | ||
| return <WrappedComponent {...(props as P)} /> | ||
| } | ||
| if (!config.commerceApi || !config.commerceApi?.parameters) { | ||
| logger.error( | ||
| 'CommerceApiProvider is not installed and no commerceApi config is provided, this extension may not work as expected.' | ||
| ) | ||
| return <WrappedComponent {...(props as P)} /> | ||
| } | ||
| const appOrigin = getAppOrigin() | ||
| return ( | ||
| <CommerceApiProvider | ||
| shortCode={config.commerceApi.parameters.shortCode} | ||
| clientId={config.commerceApi.parameters.clientId} | ||
| organizationId={config.commerceApi.parameters.organizationId} | ||
| siteId={config.commerceApi.parameters.siteId} | ||
| locale={config.commerceApi.parameters.locale} | ||
| currency={config.commerceApi.parameters.currency} | ||
| redirectURI={`${appOrigin}/callback`} | ||
| proxy={`${appOrigin}${config.commerceApi.proxyPath}`} | ||
| > | ||
| <WrappedComponent {...(props as P)} /> | ||
| </CommerceApiProvider> | ||
| ) | ||
| } | ||
|
|
||
| return HOC | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /* | ||
| * Copyright (c) 2025, 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 createLogger from '@salesforce/pwa-kit-runtime/utils/logger-factory' | ||
|
|
||
| export const logger = createLogger({packageName: 'extension-chakra-store-locator'}) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,13 +11,16 @@ import {RouteProps} from 'react-router-dom' | |
|
|
||
| // Platform Imports | ||
| import {ApplicationExtension} from '@salesforce/pwa-kit-extension-sdk/react' | ||
| import {applyHOCs} from '@salesforce/pwa-kit-extension-sdk/react/utils' | ||
|
|
||
| // Local Imports | ||
| import {withOptionalChakra} from './components/with-optional-chakra-provider' | ||
| import {withOptionalCommerceSdkReactProvider} from './components/with-optional-commerce-sdk-react-provider' | ||
| import {withStoreLocator} from './components/with-store-locator' | ||
| import {Config} from './types' | ||
|
|
||
| import StoreLocatorPage from './pages/store-locator' | ||
| import {logger} from './logger' | ||
| import extensionMeta from '../extension-meta.json' | ||
|
|
||
| class StoreLocatorExtension extends ApplicationExtension<Config> { | ||
|
|
@@ -29,13 +32,19 @@ class StoreLocatorExtension extends ApplicationExtension<Config> { | |
| const config = this.getConfig() | ||
|
|
||
| if (!config.supportedCountries || config.supportedCountries.length === 0) { | ||
| // TODO: use our logger | ||
| console.warn( | ||
| logger.error( | ||
| '[extension-chakra-store-locator] Missing supportedCountries, this extension will not work.' | ||
| ) | ||
| } | ||
|
|
||
| return withStoreLocator(withOptionalChakra(App), config) | ||
| const HOCs = [ | ||
| (component: React.ComponentType<any>) => withStoreLocator(component, config), | ||
| (component: React.ComponentType<any>) => | ||
| withOptionalCommerceSdkReactProvider(component, config), | ||
| (component: React.ComponentType<any>) => withOptionalChakra(component) | ||
| ] | ||
|
Comment on lines
+40
to
+45
Contributor
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. 👍 |
||
|
|
||
| return applyHOCs(App, HOCs) | ||
| } | ||
|
|
||
| extendRoutes(routes: RouteProps[]): RouteProps[] { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,17 @@ export interface UserConfig extends ApplicationExtensionConfig { | |
| countryCode: string | ||
| countryName: string | ||
| }> | ||
| commerceApi?: { | ||
| proxyPath: string | ||
| parameters: { | ||
| shortCode: string | ||
| clientId: string | ||
| organizationId: string | ||
| siteId: string | ||
| locale: string | ||
| currency: string | ||
| } | ||
| } | ||
|
Comment on lines
+24
to
+34
Contributor
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. 👍 |
||
| } | ||
|
|
||
| /** | ||
|
|
||
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.
I know this comment isn't related to what you are implementing in your PR.. but seeing
anytriggered me to ask a question.Uh oh!
There was an error while loading. Please reload this page.
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.
I'm not sure if there is a better option for this, maybe I should use
unknown? since the underlying wrapped component props is unknown?