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

[commerce-sdk-react] Multisite support #769

Merged
merged 14 commits into from
Oct 20, 2022
16 changes: 16 additions & 0 deletions packages/commerce-sdk-react/src/auth/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ jest.mock('./storage', () => {
},
get(key: string) {
return map.get(key)
},
delete(key: string) {
map.delete(key)
}
}
})
Expand Down Expand Up @@ -149,6 +152,19 @@ describe('Auth', () => {
// @ts-expect-error private method
expect(() => auth.isTokenExpired()).toThrow()
})
test('site switch clears auth storage', () => {
const auth = new Auth(config)
// @ts-expect-error private method
auth.set('access_token', '123')
// @ts-expect-error private method
auth.set('refresh_token_guest', '456')
const switchSiteConfig = {...config, siteId: 'another site'}
const newAuth = new Auth(switchSiteConfig)
// @ts-expect-error private method
expect(newAuth.get('access_token')).not.toBe('123')
// @ts-expect-error private method
expect(newAuth.get('refresh_token_guest')).not.toBe('456')
})
test('isTokenExpired', () => {
const auth = new Auth(config)
const JWTNotExpired = jwt.sign({exp: Math.floor(Date.now() / 1000) + 1000}, 'secret')
Expand Down
25 changes: 25 additions & 0 deletions packages/commerce-sdk-react/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type AuthDataKeys =
| 'refresh_token_registered'
| 'token_type'
| 'usid'
| 'site_id'
type AuthDataMap = Record<
AuthDataKeys,
{
Expand Down Expand Up @@ -101,6 +102,10 @@ const DATA_MAP: AuthDataMap = {
callback: () => {
cookieStorage.delete('cc-nx-g')
}
},
site_id: {
storage: cookieStorage,
key: 'cc-site-id'
}
}

Expand Down Expand Up @@ -131,6 +136,19 @@ class Auth {
fetchOptions: config.fetchOptions
})

if (this.get('site_id') && this.get('site_id') !== config.siteId) {
// if site is switched, remove all existing auth data in storage
// and the next auth.ready() call with restart the auth flow
this.clearStorage()
this.pendingToken = undefined
}

if (!this.get('site_id')) {
this.set('site_id', config.siteId, {
expires: this.REFRESH_TOKEN_EXPIRATION_DAYS
})
}

this.redirectURI = config.redirectURI
}

Expand All @@ -146,6 +164,13 @@ class Auth {
DATA_MAP[name].callback?.()
}

private clearStorage() {
Object.keys(DATA_MAP).forEach((key) => {
type Key = keyof AuthDataMap
DATA_MAP[key as Key].storage.delete(DATA_MAP[key as Key].key)
})
}

/**
* Every method in this class that returns a `TokenResponse` constructs it via this getter.
*/
Expand Down
11 changes: 11 additions & 0 deletions packages/commerce-sdk-react/src/hooks/ShopperProducts/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
import {ApiClients, Argument, DataType} from '../types'
import {useQuery} from '../useQuery'
import useConfig from '../useConfig'
import {UseQueryOptions, UseQueryResult} from '@tanstack/react-query'

type Client = ApiClients['shopperProducts']
Expand Down Expand Up @@ -33,6 +34,9 @@ function useProducts(
options?: UseQueryOptions<DataType<Client['getProducts']> | Response, Error>
): UseQueryResult<DataType<Client['getProducts']> | Response, Error> {
const {headers, rawResponse, ...parameters} = arg
const {locale, currency} = useConfig()
parameters.locale = parameters.locale || locale
parameters.currency = parameters.currency || currency
Comment on lines +37 to +39
Copy link
Contributor

Choose a reason for hiding this comment

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

I see.. we haven't been using and passing in the locale until now 👍

This change is worthy to share with everyone in the team. For every hooks we're working on, we'll need to make sure to pass in the locale, if it's supported for that particular API endpoint.

return useQuery(
['products', arg],
(_, {shopperProducts}) => {
Expand Down Expand Up @@ -65,6 +69,9 @@ function useProduct(
options?: UseQueryOptions<DataType<Client['getProduct']> | Response, Error>
): UseQueryResult<DataType<Client['getProduct']> | Response, Error> {
const {headers, rawResponse, ...parameters} = arg
const {locale, currency} = useConfig()
parameters.locale = parameters.locale || locale
parameters.currency = parameters.currency || currency
return useQuery(
['product', arg],
(_, {shopperProducts}) => {
Expand Down Expand Up @@ -100,6 +107,8 @@ function useCategories(
options?: UseQueryOptions<DataType<Client['getCategories']> | Response, Error>
): UseQueryResult<DataType<Client['getCategories']> | Response, Error> {
const {headers, rawResponse, ...parameters} = arg
const {locale} = useConfig()
parameters.locale = parameters.locale || locale
return useQuery(
['categories', arg],
(_, {shopperProducts}) => {
Expand Down Expand Up @@ -137,6 +146,8 @@ function useCategory(
options?: UseQueryOptions<DataType<Client['getCategory']> | Response, Error>
): UseQueryResult<DataType<Client['getCategory']> | Response, Error> {
const {headers, rawResponse, ...parameters} = arg
const {locale} = useConfig()
parameters.locale = parameters.locale || locale
return useQuery(
['category', arg],
(_, {shopperProducts}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import {ApiClients, Argument, DataType} from '../types'
import {useQuery} from '../useQuery'
import {UseQueryOptions, UseQueryResult} from '@tanstack/react-query'
import useConfig from '../useConfig'

type Client = ApiClients['shopperSearch']

Expand Down Expand Up @@ -37,6 +38,9 @@ function useProductSearch(
options?: UseQueryOptions<DataType<Client['productSearch']> | Response, Error>
): UseQueryResult<DataType<Client['productSearch']> | Response, Error> {
const {headers, rawResponse, ...parameters} = arg
const {locale, currency} = useConfig()
parameters.locale = parameters.locale || locale
parameters.currency = parameters.currency || currency
return useQuery(
['productSearch', arg],
(_, {shopperSearch}) => shopperSearch.productSearch({parameters, headers}, rawResponse),
Expand Down Expand Up @@ -72,6 +76,9 @@ function useSearchSuggestions(
options?: UseQueryOptions<DataType<Client['getSearchSuggestions']> | Response, Error>
): UseQueryResult<DataType<Client['getSearchSuggestions']> | Response, Error> {
const {headers, rawResponse, ...parameters} = arg
const {locale, currency} = useConfig()
parameters.locale = parameters.locale || locale
parameters.currency = parameters.currency || currency
return useQuery(
['search-suggestions', arg],
(_, {shopperSearch}) =>
Expand Down
18 changes: 18 additions & 0 deletions packages/commerce-sdk-react/src/hooks/useConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2022, 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 {ConfigContext, CommerceApiProviderProps} from '../provider'

/**
* @Internal
*/
const useConfig = (): Omit<CommerceApiProviderProps, 'children'> => {
return React.useContext(ConfigContext)
}

export default useConfig
32 changes: 24 additions & 8 deletions packages/commerce-sdk-react/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* 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, {Fragment, ReactElement, useEffect, useMemo} from 'react'
import React, {ReactElement, useEffect, useMemo} from 'react'
import {
ShopperBaskets,
ShopperContexts,
Expand Down Expand Up @@ -37,6 +37,11 @@ export interface CommerceApiProviderProps extends ApiClientConfigParams {
*/
export const CommerceApiContext = React.createContext({} as ApiClients)

/**
* @internal
*/
export const ConfigContext = React.createContext({} as Omit<CommerceApiProviderProps, 'children'>)

/**
* @internal
*/
Expand All @@ -58,7 +63,9 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => {
redirectURI,
fetchOptions,
siteId,
shortCode
shortCode,
locale,
currency
} = props

const config = {
Expand All @@ -73,7 +80,6 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => {
throwOnBadResponse: true,
fetchOptions
}

const apiClients = useMemo(() => {
return {
shopperBaskets: new ShopperBaskets(config),
Expand Down Expand Up @@ -105,16 +111,26 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => {
auth.ready()
}, [auth])

// TODO: wrap the children with:
// - context for enabling useServerEffect hook
// - context for sharing the auth object that would manage the tokens -> this will probably be for internal use only
return (
<Fragment>
<ConfigContext.Provider
value={{
clientId,
headers,
organizationId,
proxy,
redirectURI,
fetchOptions,
siteId,
shortCode,
locale,
currency
}}
>
<CommerceApiContext.Provider value={apiClients}>
<AuthContext.Provider value={auth}>{children}</AuthContext.Provider>
</CommerceApiContext.Provider>
<ReactQueryDevtools />
</Fragment>
</ConfigContext.Provider>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* 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, {ReactElement} from 'react'
import React, {useState, ReactElement} from 'react'
// @ts-ignore
import {CommerceApiProvider} from 'commerce-sdk-react'
// @ts-ignore
Expand All @@ -21,19 +21,50 @@ const AppConfig = (props: AppConfigProps): ReactElement => {
const headers = {
'correlation-id': correlationId
}
const defaultSiteId = 'RefArchGlobal'
const defaultLocale = 'en-US'
const [siteId, setSiteId] = useState(defaultSiteId)
const [locale, setLocale] = useState(defaultLocale)
const anotherSite = siteId === defaultSiteId ? 'RefArch' : defaultSiteId
const anotherLocale = locale === defaultLocale ? 'en-CA' : defaultLocale
return (
<CommerceApiProvider
siteId="RefArchGlobal"
siteId={siteId}
shortCode="8o7m175y"
clientId="c9c45bfd-0ed3-4aa2-9971-40f88962b836"
organizationId="f_ecom_zzrf_001"
redirectURI="http://localhost:3000/callback"
proxy="http://localhost:3000/mobify/proxy/api"
locale="en-US"
locale={locale}
currency="USD"
headers={headers}
>
{props.children}
<div
style={{
backgroundColor: '#ebebeb',
position: 'fixed',
right: 0,
bottom: 0,
margin: '8px'
}}
>
<h3>Site: {siteId}</h3>
<button
onClick={() => {
setSiteId(anotherSite)
}}
>
Switch to {anotherSite}
</button>
<button
onClick={() => {
setLocale(anotherLocale)
}}
>
Switch to {anotherLocale}
</button>
</div>
</CommerceApiProvider>
)
}
Expand Down