Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions packages/commerce-sdk-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@
},
"dependencies": {
"@salesforce/storefront-next-runtime": "0.1.1",
"commerce-sdk-isomorphic": "5.1.0",
"commerce-sdk-isomorphic": "5.1.0-unstable-20260320081529",
"js-cookie": "^3.0.1",
"jwt-decode": "^4.0.0"
},
"devDependencies": {
"@salesforce/pwa-kit-dev": "3.18.0-dev",
"@salesforce/pwa-kit-dev": "3.17.0",
"@tanstack/react-query": "^4.28.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
Expand All @@ -61,7 +61,7 @@
"@types/react-helmet": "~6.1.6",
"@types/react-router-dom": "~5.3.3",
"cross-env": "^5.2.1",
"internal-lib-build": "3.18.0-dev",
"internal-lib-build": "3.17.0",
"jsonwebtoken": "^9.0.0",
"nock": "^13.3.0",
"nodemon": "^2.0.22",
Expand Down
1 change: 1 addition & 0 deletions packages/commerce-sdk-react/src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const EXCLUDE_COOKIE_SUFFIX = [DWSID_COOKIE_NAME, DNT_COOKIE_NAME]
export const SERVER_AFFINITY_HEADER_KEY = 'sfdc_dwsid'

export const CLIENT_KEYS = {
SHOPPER_AGENTS: 'shopperAgents',
SHOPPER_BASKETS: 'shopperBaskets',
SHOPPER_BASKETS_V2: 'shopperBasketsV2',
SHOPPER_CONFIGURATIONS: 'shopperConfigurations',
Expand Down
17 changes: 17 additions & 0 deletions packages/commerce-sdk-react/src/hooks/ShopperAgents/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 {ApiClients, CacheUpdateMatrix} from '../types'
import {CLIENT_KEYS} from '../../constant'

const noop = () => ({})

const CLIENT_KEY = CLIENT_KEYS.SHOPPER_AGENTS
type Client = NonNullable<ApiClients[typeof CLIENT_KEY]>

export const cacheUpdateMatrix: CacheUpdateMatrix<Client> = {
postSessionInit: noop
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 {ShopperAgents} from 'commerce-sdk-isomorphic'
import {getUnimplementedEndpoints} from '../../test-utils'
import {cacheUpdateMatrix} from './cache'
import {ShopperAgentsMutations as mutations} from './mutation'

describe('Shopper Agents hooks', () => {
test('all endpoints have hooks', () => {
// unimplemented = SDK method exists, but no query hook or value in mutations enum
const unimplemented = getUnimplementedEndpoints(ShopperAgents, {}, mutations)
// If this test fails: create a new query hook, add the endpoint to the mutations enum,
// or add it to the `expected` array with a comment explaining "TODO" or "never" (and why).
expect(unimplemented).toEqual([])
})
test('all mutations have cache update logic', () => {
// unimplemented = value in mutations enum, but no method in cache update matrix
const unimplemented = new Set<string>(Object.values(mutations))
Object.entries(cacheUpdateMatrix).forEach(([method, implementation]) => {
if (implementation) unimplemented.delete(method)
})
// If this test fails: add cache update logic, remove the endpoint from the mutations enum,
// or add it to the `expected` array to indicate that it is still a TODO.
expect([...unimplemented]).toEqual([])
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* 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
*/
export * from './mutation'
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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 {act} from '@testing-library/react'
import nock from 'nock'

import {ApiClients, Argument} from '../types'
import {ShopperAgentsMutation, useShopperAgentsMutation} from './mutation'
import {
mockMutationEndpoints,
renderHookWithProviders,
waitAndExpectSuccess
} from '../../test-utils'
import {CLIENT_KEYS} from '../../constant'

jest.mock('../../auth/index.ts', () => {
return jest.fn().mockImplementation(() => ({
ready: jest.fn().mockResolvedValue({access_token: 'access_token'}),
get: jest.fn().mockResolvedValue({dwsid: 'dw-session-123'})
}))
})

const CLIENT_KEY = CLIENT_KEYS.SHOPPER_AGENTS
type Client = NonNullable<ApiClients[typeof CLIENT_KEY]>

const sessionInitEndpoint = '/shopper/shopper-agents/'

const PARAMETERS = {
organizationId: 'f_ecom_zzrf_001'
} as const

const createOptions = <Mut extends ShopperAgentsMutation>(
body: Argument<Client[Mut]> extends {body: infer B} ? B : undefined
): Argument<Client[Mut]> => ({parameters: PARAMETERS, body} as Argument<Client[Mut]>)

const sessionInitRequest = {
sessionInitKey: 'aPrx.0rn0R1k7G.PtUUXh6S90_rMvqsYhpDZlNpyYsLsTm8KYLodM3flsRBbCD'
}

describe('Shopper Agents mutation hooks', () => {
beforeEach(() => nock.cleanAll())

test('`postSessionInit` succeeds with 202 response', async () => {
const options = createOptions<'postSessionInit'>(sessionInitRequest)
// Mock endpoint - 202 response with no body
mockMutationEndpoints(sessionInitEndpoint, undefined, 202)

const {result} = renderHookWithProviders(() => ({
mutation: useShopperAgentsMutation('postSessionInit')
}))

// Do mutation
act(() => result.current.mutation.mutate(options))
await waitAndExpectSuccess(() => result.current.mutation)
// 202 Accepted returns undefined/no response body
expect(result.current.mutation.data).toBeUndefined()
})
})
66 changes: 66 additions & 0 deletions packages/commerce-sdk-react/src/hooks/ShopperAgents/mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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 {ApiClients, ApiMethod, Argument, CacheUpdateGetter, DataType, MergedOptions} from '../types'
import {useMutation} from '../useMutation'
import {UseMutationResult} from '@tanstack/react-query'
import {NotImplementedError} from '../utils'
import {cacheUpdateMatrix} from './cache'
import {CLIENT_KEYS} from '../../constant'
import useCommerceApi from '../useCommerceApi'

const CLIENT_KEY = CLIENT_KEYS.SHOPPER_AGENTS
type Client = NonNullable<ApiClients[typeof CLIENT_KEY]>

/**
* Mutations available for Shopper Agents
* @group ShopperAgents
* @category Mutation
* @enum
*/
export const ShopperAgentsMutations = {
/**
* Initializes an Agentforce session. The request body must include the sessionInitKey field.
* Missing or invalid sessionInitKey information results in INVALID_REQUEST_PARAMETERS (400).
* @returns A TanStack Query mutation hook for interacting with the Shopper Agents `postSessionInit` endpoint.
*/
PostSessionInit: 'postSessionInit'
} as const

/**
* Mutation for Shopper Agents.
* @group ShopperAgents
* @category Mutation
*/
export type ShopperAgentsMutation =
(typeof ShopperAgentsMutations)[keyof typeof ShopperAgentsMutations]

/**
* Mutation hook for Shopper Agents.
* @group ShopperAgents
* @category Mutation
*/
export function useShopperAgentsMutation<Mutation extends ShopperAgentsMutation>(
mutation: Mutation
): UseMutationResult<DataType<Client[Mutation]>, unknown, Argument<Client[Mutation]>> {
const getCacheUpdates = cacheUpdateMatrix[mutation]
// TODO: Remove this check when all mutations are implemented.
if (!getCacheUpdates) throw new NotImplementedError(`The '${mutation}' mutation`)

// The `Options` and `Data` types for each mutation are similar, but distinct, and the union
// type generated from `Client[Mutation]` seems to be too complex for TypeScript to handle.
// I'm not sure if there's a way to avoid the type assertions in here for the methods that
// use them. However, I'm fairly confident that they are safe to do, as they seem to be simply
// re-asserting what we already have.
const client = useCommerceApi(CLIENT_KEY)
type Options = Argument<Client[Mutation]>
type Data = DataType<Client[Mutation]>
return useMutation({
client,
method: (opts: Options) => (client[mutation] as ApiMethod<Options, Data>)(opts),
getCacheUpdates: getCacheUpdates as unknown as CacheUpdateGetter<MergedOptions<Client, Options>, Data>
})
}
1 change: 1 addition & 0 deletions packages/commerce-sdk-react/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +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
*/
export * from './ShopperAgents'
export * from './ShopperBaskets'
// V2 — available under explicit V2 names
export {
Expand Down
2 changes: 2 additions & 0 deletions packages/commerce-sdk-react/src/hooks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
import {InvalidateQueryFilters, QueryFilters, Updater, UseQueryOptions} from '@tanstack/react-query'
import {
ShopperAgents,
ShopperBaskets,
ShopperBasketsV2,
ShopperConfigurations,
Expand Down Expand Up @@ -88,6 +89,7 @@ export type ApiClientConfigParams = {
* A map of commerce-sdk-isomorphic API client instances.
*/
export interface ApiClients {
shopperAgents?: ShopperAgents<ApiClientConfigParams>
shopperBaskets?: ShopperBaskets<ApiClientConfigParams>
shopperBasketsV2?: ShopperBasketsV2<ApiClientConfigParams>
shopperConsents?: ShopperConsents<ApiClientConfigParams>
Expand Down
2 changes: 2 additions & 0 deletions packages/commerce-sdk-react/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {ApiClientConfigParams, ApiClients, SDKClientTransformer} from './hooks/t
import {Logger} from './types'
import {DWSID_COOKIE_NAME, SERVER_AFFINITY_HEADER_KEY} from './constant'
import {
ShopperAgents,
ShopperBaskets,
ShopperBasketsV2,
ShopperConsents,
Expand Down Expand Up @@ -267,6 +268,7 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => {
}

return {
shopperAgents: new ShopperAgents(config),
shopperBaskets: new ShopperBaskets(config),
shopperBasketsV2: new ShopperBasketsV2(config),
shopperConsents: new ShopperConsents(config),
Expand Down
Loading