Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8190025
Make API clients injectable to allow tree-shaking
shethj May 30, 2025
cbd8e1e
Update changelog
shethj May 30, 2025
7fa34b2
Implemented commerce API provider proxy
shethj Jun 4, 2025
437e164
Allow promise based response from the transformer
shethj Jun 4, 2025
5f99885
Restore comment
shethj Jun 4, 2025
6138000
Revert lint change
shethj Jun 4, 2025
faf0069
Revert template changes
shethj Jun 5, 2025
03fbbb6
Revert template changes
shethj Jun 5, 2025
1b80c10
Revert template changes
shethj Jun 5, 2025
b0b0e20
Extract injector types out of provider
shethj Jun 5, 2025
749baef
Merge branch 'develop' into feature/commerce-sdk-react-refactor-2
shethj Jun 5, 2025
b6fb54a
Fix linting and add test cases
shethj Jun 5, 2025
fff68c0
Trigger CI commit
shethj Jun 5, 2025
0ab0673
Trigger CI commit
shethj Jun 5, 2025
8c7507c
Restore isomorphic SDK
shethj Jun 5, 2025
5d74541
Rename withParameterInjection to be more self-explanatory
shethj Jun 6, 2025
7e7aada
Merge branch 'develop' into feature/commerce-sdk-react-refactor-2
shethj Jun 6, 2025
76c6cbf
Fix linting
shethj Jun 6, 2025
3698a4a
Merge branch 'develop' into feature/commerce-sdk-react-refactor-2
shethj Jun 6, 2025
576792a
Update changelog to point to correct PR
shethj Jun 6, 2025
dbc0599
Merge branch 'develop' into feature/commerce-sdk-react-refactor-2
shethj Jun 9, 2025
78f0a13
Add apiClients as dependency
shethj Jun 10, 2025
95af8a5
Remove passing children to transformSDKClient
shethj Jun 10, 2025
149df85
Merge branch 'develop' into feature/commerce-sdk-react-refactor-2
shethj Jun 10, 2025
b6b5f11
Merge branch 'develop' into feature/commerce-sdk-react-refactor-2
shethj Jun 11, 2025
9d95e11
Merge branch 'develop' into feature/commerce-sdk-react-refactor-2
shethj Jun 12, 2025
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
2 changes: 2 additions & 0 deletions packages/commerce-sdk-react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## v3.4.0-dev.0 (May 23, 2025)
- Now compatible with either React 17 and 18 [#2506](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2506)

- Refactor commerce-sdk-react to allow injecting ApiClients [#2519](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2519)

## v3.3.0 (May 22, 2025)

- Fix inconsistency between dwsid and access token for guest login when hybrid authentication is enabled [#2397](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2397)
Expand Down
15 changes: 15 additions & 0 deletions packages/commerce-sdk-react/src/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ShopperStores
} from 'commerce-sdk-isomorphic'
import {helpers} from 'commerce-sdk-isomorphic'
import {CommerceApiProviderProps} from '../provider'

// --- GENERAL UTILITIES --- //

Expand Down Expand Up @@ -226,3 +227,17 @@ export type TMutationVariables = {
parameters?: {[key: string]: string | number | boolean | string[] | number[]}
headers?: {[key: string]: string}
} | void

export type SDKClientTransformer<T> = (
params: T,
methodName: string,
options: any
) => any | Promise<any>
Comment on lines +231 to +235
Copy link
Contributor

Choose a reason for hiding this comment

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

From what I see in the default transformer, it looks like the return object is like options but with some overrides/additions. If that's the case, can we make the type clearer?

Suggested change
export type SDKClientTransformer<T> = (
params: T,
methodName: string,
options: any
) => any | Promise<any>
export type SDKClientTransformer<T> = (
params: T,
methodName: string,
options: SomeOptions
) => SomeOptions | Promise<SomeOptions>


export type ErrorCallback<TParams> = (methodName: string, error: any, params: TParams) => void

export interface SDKClientTransformConfig<TParams = Record<string, any>> {
props: Omit<CommerceApiProviderProps, 'children'>
transformer?: SDKClientTransformer<TParams>
onError?: ErrorCallback<TParams>
}
2 changes: 1 addition & 1 deletion packages/commerce-sdk-react/src/hooks/useConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {ConfigContext, CommerceApiProviderProps} from '../provider'
/**
* @Internal
*/
const useConfig = (): Omit<CommerceApiProviderProps, 'children'> => {
const useConfig = (): Omit<CommerceApiProviderProps, 'children' | 'apiClients'> => {
return React.useContext(ConfigContext)
}

Expand Down
108 changes: 75 additions & 33 deletions packages/commerce-sdk-react/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React, {ReactElement, useEffect, useMemo} from 'react'
import Auth from './auth'
import {ApiClientConfigParams, ApiClients, SDKClientTransformer} from './hooks/types'
import {Logger} from './types'
import {
DWSID_COOKIE_NAME,
MOBIFY_PATH,
SERVER_AFFINITY_HEADER_KEY,
SLAS_PRIVATE_PROXY_PATH
} from './constant'
import {
ShopperBaskets,
ShopperContexts,
Expand All @@ -20,15 +29,8 @@ import {
ShopperBasketsTypes,
ShopperStores
} from 'commerce-sdk-isomorphic'
import Auth from './auth'
import {ApiClientConfigParams, ApiClients} from './hooks/types'
import {Logger} from './types'
import {
DWSID_COOKIE_NAME,
MOBIFY_PATH,
SERVER_AFFINITY_HEADER_KEY,
SLAS_PRIVATE_PROXY_PATH
} from './constant'
import {transformSDKClient} from './utils'

export interface CommerceApiProviderProps extends ApiClientConfigParams {
children: React.ReactNode
proxy: string
Expand All @@ -46,6 +48,7 @@ export interface CommerceApiProviderProps extends ApiClientConfigParams {
passwordlessLoginCallbackURI?: string
refreshTokenRegisteredCookieTTL?: number
refreshTokenGuestCookieTTL?: number
apiClients?: ApiClients
}

/**
Expand All @@ -56,7 +59,9 @@ export const CommerceApiContext = React.createContext({} as ApiClients)
/**
* @internal
*/
export const ConfigContext = React.createContext({} as Omit<CommerceApiProviderProps, 'children'>)
export const ConfigContext = React.createContext(
{} as Omit<CommerceApiProviderProps, 'children' | 'apiClients'>
)

/**
* @internal
Expand Down Expand Up @@ -126,7 +131,8 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => {
defaultDnt,
passwordlessLoginCallbackURI,
refreshTokenRegisteredCookieTTL,
refreshTokenGuestCookieTTL
refreshTokenGuestCookieTTL,
apiClients
} = props

// Set the logger based on provided configuration, or default to the console object if no logger is provided
Expand Down Expand Up @@ -167,7 +173,8 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => {
defaultDnt,
passwordlessLoginCallbackURI,
refreshTokenRegisteredCookieTTL,
refreshTokenGuestCookieTTL
refreshTokenGuestCookieTTL,
apiClients
])

const dwsid = auth.get(DWSID_COOKIE_NAME)
Expand All @@ -176,28 +183,62 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => {
serverAffinityHeader[SERVER_AFFINITY_HEADER_KEY] = dwsid
}

const config = {
proxy,
headers: {
...headers,
...serverAffinityHeader
},
parameters: {
clientId,
organizationId,
shortCode,
siteId,
locale,
currency
},
throwOnBadResponse: true,
fetchOptions
const _defaultTransformer: SDKClientTransformer<Record<string, any>> = (_, _$, options) => {
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 the first 2 arguments to this function are not being used. So do we really need them?

Besides, it doesn't look like there's a way for the end user to pass in their own transformer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah v2 integration is in a separate PR.

I see the first 2 arguments to this function are not being used. So do we really need them?

We're not using those arguments now: those 2 are props and methodName but we might need them for some future change. I could convert the props to transformer to be an object with optional properties to make it clear.

Also, we're not accepting a transformer from the end user for now is because they already have props in CommerceApiProvider where they can pass in all the different headers and options and our default transformer and SDK initialization already accepts all that when creating an instance.

However, the transformSDKClient function is open to accept any transformer function so in the future if we come across a use-case to allow the customer to pass in a transformer we can easily do that by simply creating another options prop for commerceApi.

I haven't added that today because I don't see a direct use-case for that right now.

return {
...options,
headers: {
...options.headers,
...serverAffinityHeader
},
throwOnBadResponse: true,
fetchOptions: {
...options.fetchOptions,
...fetchOptions
}
}
}

const baseUrl = config.proxy.split(MOBIFY_PATH)[0]
const privateClientEndpoint = `${baseUrl}${SLAS_PRIVATE_PROXY_PATH}`
const updatedClients: ApiClients = useMemo(() => {
if (apiClients) {
const clients: Record<string, any> = {}

// transformSDKClient is simply a utility function that wraps the SDK Client instance
// in a Proxy that allows us to transform the method arguments and modify headers, parameters, and other options.
// We don't really need to pass in the children prop to the transformer function, so we'll just pass in the rest of the props.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {children, ...restProps} = props

Object.entries(apiClients ?? {}).forEach(([key, apiClient]) => {
clients[key] = transformSDKClient(apiClient, {
props: restProps,
transformer: _defaultTransformer
})
})

return clients as ApiClients
}

const config = {
proxy,
headers: {
...headers,
...serverAffinityHeader
},
parameters: {
clientId,
organizationId,
shortCode,
siteId,
locale,
currency
},
throwOnBadResponse: true,
fetchOptions
}

const baseUrl = config.proxy.split(MOBIFY_PATH)[0]
const privateClientEndpoint = `${baseUrl}${SLAS_PRIVATE_PROXY_PATH}`

const apiClients = useMemo(() => {
return {
shopperBaskets: new ShopperBaskets(config),
shopperContexts: new ShopperContexts(config),
Expand All @@ -224,7 +265,8 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => {
fetchOptions,
locale,
currency,
headers?.['correlation-id']
headers?.['correlation-id'],
apiClients
])

// Initialize the session
Expand All @@ -251,7 +293,7 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => {
refreshTokenGuestCookieTTL
}}
>
<CommerceApiContext.Provider value={apiClients}>
<CommerceApiContext.Provider value={updatedClients}>
<AuthContext.Provider value={auth}>{children}</AuthContext.Provider>
</CommerceApiContext.Provider>
</ConfigContext.Provider>
Expand Down
Loading
Loading