diff --git a/packages/commerce-sdk-react/CHANGELOG.md b/packages/commerce-sdk-react/CHANGELOG.md index 184ca75c6a..b0523d6f60 100644 --- a/packages/commerce-sdk-react/CHANGELOG.md +++ b/packages/commerce-sdk-react/CHANGELOG.md @@ -1,5 +1,6 @@ ## v5.1.0-dev - Add Node 24 support. Drop Node 16 support. [#3652](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3652) +- Add Shopper Consents API support [#3674](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3674) ## v5.0.0 (Feb 12, 2026) - Upgrade to commerce-sdk-isomorphic v5.0.0 and introduce Payment Instrument SCAPI integration [#3552](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3552) @@ -14,7 +15,7 @@ ## v4.2.0 (Nov 04, 2025) - Upgrade to commerce-sdk-isomorphic v4.0.1 [#3449](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3449) -- Prevent headers from being overriden in `generateCustomEndpointOptions` [#3405](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3405/) +- Prevent headers from being overridden in `generateCustomEndpointOptions` [#3405](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3405/) ## v4.1.0 (Sep 25, 2025) diff --git a/packages/commerce-sdk-react/src/constant.ts b/packages/commerce-sdk-react/src/constant.ts index 2e6b332995..a11b3c0237 100644 --- a/packages/commerce-sdk-react/src/constant.ts +++ b/packages/commerce-sdk-react/src/constant.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Salesforce, Inc. + * 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 @@ -46,6 +46,7 @@ export const SERVER_AFFINITY_HEADER_KEY = 'sfdc_dwsid' export const CLIENT_KEYS = { SHOPPER_BASKETS: 'shopperBaskets', + SHOPPER_CONSENTS: 'shopperConsents', SHOPPER_CONTEXTS: 'shopperContexts', SHOPPER_CUSTOMERS: 'shopperCustomers', SHOPPER_EXPERIENCE: 'shopperExperience', diff --git a/packages/commerce-sdk-react/src/hooks/ShopperConsents/cache.ts b/packages/commerce-sdk-react/src/hooks/ShopperConsents/cache.ts new file mode 100644 index 0000000000..b0323cf46a --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperConsents/cache.ts @@ -0,0 +1,29 @@ +/* + * 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 {getSubscriptions} from './queryKeyHelpers' +import {CLIENT_KEYS} from '../../constant' + +const CLIENT_KEY = CLIENT_KEYS.SHOPPER_CONSENTS +type Client = NonNullable + +export const cacheUpdateMatrix: CacheUpdateMatrix = { + updateSubscription(customerId, {parameters}, response) { + // When a subscription is updated, we invalidate the getSubscriptions query + // to ensure the UI reflects the latest subscription state + return { + invalidate: [{queryKey: getSubscriptions.queryKey(parameters)}] + } + }, + updateSubscriptions(customerId, {parameters}, response) { + // When multiple subscriptions are updated, we invalidate the getSubscriptions query + // to ensure the UI reflects the latest subscription states + return { + invalidate: [{queryKey: getSubscriptions.queryKey(parameters)}] + } + } +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperConsents/index.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperConsents/index.test.ts new file mode 100644 index 0000000000..c46e5bd8df --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperConsents/index.test.ts @@ -0,0 +1,31 @@ +/* + * 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 {ShopperConsents} from 'commerce-sdk-isomorphic' +import {getUnimplementedEndpoints} from '../../test-utils' +import {cacheUpdateMatrix} from './cache' +import {ShopperConsentsMutations as mutations} from './mutation' +import * as queries from './query' + +describe('Shopper Consents hooks', () => { + test('all endpoints have hooks', () => { + // unimplemented = SDK method exists, but no query hook or value in mutations enum + const unimplemented = getUnimplementedEndpoints(ShopperConsents, queries, 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(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([]) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperConsents/index.ts b/packages/commerce-sdk-react/src/hooks/ShopperConsents/index.ts new file mode 100644 index 0000000000..0d56f4873c --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperConsents/index.ts @@ -0,0 +1,8 @@ +/* + * 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' +export * from './query' diff --git a/packages/commerce-sdk-react/src/hooks/ShopperConsents/mutation.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperConsents/mutation.test.ts new file mode 100644 index 0000000000..10e699fd97 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperConsents/mutation.test.ts @@ -0,0 +1,271 @@ +/* + * 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, waitFor} from '@testing-library/react' +import {ShopperConsentsTypes} from 'commerce-sdk-isomorphic' +import nock from 'nock' +import { + assertInvalidateQuery, + mockMutationEndpoints, + mockQueryEndpoint, + renderHookWithProviders, + waitAndExpectSuccess +} from '../../test-utils' +import {ShopperConsentsMutation, useShopperConsentsMutation} from '../ShopperConsents' +import {ApiClients, Argument} from '../types' +import * as queries from './query' +import {CLIENT_KEYS} from '../../constant' + +jest.mock('../../auth/index.ts', () => { + const {default: mockAuth} = jest.requireActual('../../auth/index.ts') + mockAuth.prototype.ready = jest.fn().mockResolvedValue({access_token: 'access_token'}) + return mockAuth +}) + +const CLIENT_KEY = CLIENT_KEYS.SHOPPER_CONSENTS +type Client = NonNullable +const consentsEndpoint = '/shopper/shopper-consents/' +/** All Shopper Consents parameters. Can be used for all endpoints, as unused params are ignored. */ +const PARAMETERS = { + siteId: 'RefArch' +} as const +/** Options object that can be used for all query endpoints. */ +const queryOptions = {parameters: PARAMETERS} + +const baseSubscriptionResponse: ShopperConsentsTypes.ConsentSubscriptionResponse = { + data: [ + { + subscriptionId: 'test-subscription', + channels: ['email' as ShopperConsentsTypes.ChannelType], + contactPointValue: 'test@example.com' + } + ] +} + +type Mutation = ShopperConsentsMutation +/** Map of mutation method name to test options. */ +type TestMap = {[Mut in Mutation]?: Argument} +const testMap: TestMap = { + updateSubscription: { + parameters: PARAMETERS, + body: { + subscriptionId: 'test-subscription', + channel: 'email', + status: 'opt_in', + contactPointValue: 'test@example.com' + } + }, + updateSubscriptions: { + parameters: PARAMETERS, + body: { + subscriptions: [ + { + subscriptionId: 'test-subscription', + channel: 'email' as ShopperConsentsTypes.ChannelType, + status: 'opt_in' as ShopperConsentsTypes.ConsentStatus, + contactPointValue: 'test@example.com' + } + ] + } + } +} +// Type assertion is necessary because `Object.entries` is limited +type TestCase = [Mutation, NonNullable] +const testCases = Object.entries(testMap) as TestCase[] + +describe('Shopper Consents mutations', () => { + beforeEach(() => nock.cleanAll()) + test.each(testCases)('`%s` returns data on success', async (mutationName, options) => { + mockMutationEndpoints(consentsEndpoint, {}) + const {result} = renderHookWithProviders(() => { + return useShopperConsentsMutation(mutationName) + }) + act(() => result.current.mutate(options)) + await waitAndExpectSuccess(() => result.current) + }) +}) + +describe('Cache update behavior', () => { + describe('updateSubscription', () => { + beforeEach(() => nock.cleanAll()) + + test('invalidates `getSubscriptions` query', async () => { + mockQueryEndpoint(consentsEndpoint, baseSubscriptionResponse) + mockMutationEndpoints(consentsEndpoint, {}) + mockQueryEndpoint(consentsEndpoint, baseSubscriptionResponse) + const {result} = renderHookWithProviders(() => { + return { + query: queries.useSubscriptions(queryOptions), + mutation: useShopperConsentsMutation('updateSubscription') + } + }) + await waitAndExpectSuccess(() => result.current.query) + const oldData = result.current.query.data + + act(() => + result.current.mutation.mutate({ + parameters: PARAMETERS, + body: { + subscriptionId: 'test-subscription', + channel: 'email', + status: 'opt_out', + contactPointValue: 'test@example.com' + } + }) + ) + await waitAndExpectSuccess(() => result.current.mutation) + assertInvalidateQuery(result.current.query, oldData) + }) + + test('triggers refetch after cache invalidation', async () => { + const updatedResponse: ShopperConsentsTypes.ConsentSubscriptionResponse = { + data: [ + { + subscriptionId: 'test-subscription', + channels: ['email' as ShopperConsentsTypes.ChannelType], + contactPointValue: 'test@example.com' + } + ] + } + + mockQueryEndpoint(consentsEndpoint, baseSubscriptionResponse) + mockMutationEndpoints(consentsEndpoint, {}) + mockQueryEndpoint(consentsEndpoint, updatedResponse) // Refetch returns updated data + + const {result} = renderHookWithProviders(() => { + return { + query: queries.useSubscriptions(queryOptions), + mutation: useShopperConsentsMutation('updateSubscription') + } + }) + await waitAndExpectSuccess(() => result.current.query) + expect(result.current.query.data).toEqual(baseSubscriptionResponse) + + act(() => + result.current.mutation.mutate({ + parameters: PARAMETERS, + body: { + subscriptionId: 'test-subscription', + channel: 'email', + status: 'opt_out', + contactPointValue: 'test@example.com' + } + }) + ) + await waitAndExpectSuccess(() => result.current.mutation) + + // Wait for refetch to complete + await waitAndExpectSuccess(() => result.current.query) + // After refetch, data should be updated + expect(result.current.query.data).toEqual(updatedResponse) + }) + }) + + describe('updateSubscriptions', () => { + beforeEach(() => nock.cleanAll()) + + test('invalidates `getSubscriptions` query', async () => { + mockQueryEndpoint(consentsEndpoint, baseSubscriptionResponse) + mockMutationEndpoints(consentsEndpoint, {}) + mockQueryEndpoint(consentsEndpoint, baseSubscriptionResponse) + const {result} = renderHookWithProviders(() => { + return { + query: queries.useSubscriptions(queryOptions), + mutation: useShopperConsentsMutation('updateSubscriptions') + } + }) + await waitAndExpectSuccess(() => result.current.query) + const oldData = result.current.query.data + + act(() => + result.current.mutation.mutate({ + parameters: PARAMETERS, + body: { + subscriptions: [ + { + subscriptionId: 'test-subscription', + channel: 'email' as ShopperConsentsTypes.ChannelType, + status: 'opt_out' as ShopperConsentsTypes.ConsentStatus, + contactPointValue: 'test@example.com' + }, + { + subscriptionId: 'test-subscription-2', + channel: 'sms' as ShopperConsentsTypes.ChannelType, + status: 'opt_in' as ShopperConsentsTypes.ConsentStatus, + contactPointValue: '+15551234567' + } + ] + } + }) + ) + await waitAndExpectSuccess(() => result.current.mutation) + assertInvalidateQuery(result.current.query, oldData) + }) + + test('ensures stale cache is not served after mutation', async () => { + const staleResponse = baseSubscriptionResponse + const freshResponse: ShopperConsentsTypes.ConsentSubscriptionResponse = { + data: [ + { + subscriptionId: 'test-subscription', + channels: ['email' as ShopperConsentsTypes.ChannelType], + contactPointValue: 'test@example.com' + }, + { + subscriptionId: 'test-subscription-2', + channels: ['sms' as ShopperConsentsTypes.ChannelType], + contactPointValue: '+15551234567' + } + ] + } + + mockQueryEndpoint(consentsEndpoint, staleResponse) + mockMutationEndpoints(consentsEndpoint, {}) + mockQueryEndpoint(consentsEndpoint, freshResponse) // Refetch returns fresh data + + const {result} = renderHookWithProviders(() => { + return { + query: queries.useSubscriptions(queryOptions), + mutation: useShopperConsentsMutation('updateSubscriptions') + } + }) + + // Initial fetch - get stale data + await waitAndExpectSuccess(() => result.current.query) + expect(result.current.query.data?.data).toHaveLength(1) + + // Perform mutation + act(() => + result.current.mutation.mutate({ + parameters: PARAMETERS, + body: { + subscriptions: [ + { + subscriptionId: 'test-subscription', + channel: 'email' as ShopperConsentsTypes.ChannelType, + status: 'opt_out' as ShopperConsentsTypes.ConsentStatus, + contactPointValue: 'test@example.com' + }, + { + subscriptionId: 'test-subscription-2', + channel: 'sms' as ShopperConsentsTypes.ChannelType, + status: 'opt_in' as ShopperConsentsTypes.ConsentStatus, + contactPointValue: '+15551234567' + } + ] + } + }) + ) + await waitAndExpectSuccess(() => result.current.mutation) + + // Wait for refetch to complete with fresh data + await waitFor(() => { + expect(result.current.query.data?.data).toHaveLength(2) + }) + expect(result.current.query.data).toEqual(freshResponse) + }) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperConsents/mutation.ts b/packages/commerce-sdk-react/src/hooks/ShopperConsents/mutation.ts new file mode 100644 index 0000000000..22cef56a52 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperConsents/mutation.ts @@ -0,0 +1,73 @@ +/* + * 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_CONSENTS +type Client = NonNullable + +/** + * Mutations available for Shopper Consents. + * @group ShopperConsents + * @category Mutation + * @enum + */ +export const ShopperConsentsMutations = { + /** + * Updates the customer's consent subscription for a specific channel. + * @returns A TanStack Query mutation hook for interacting with the Shopper Consents `updateSubscription` endpoint. + */ + UpdateSubscription: 'updateSubscription', + /** + * Updates multiple customer consent subscriptions. + * @returns A TanStack Query mutation hook for interacting with the Shopper Consents `updateSubscriptions` endpoint. + */ + UpdateSubscriptions: 'updateSubscriptions' +} as const + +/** + * Mutation for Shopper Consents. + * @group ShopperConsents + * @category Mutation + */ +export type ShopperConsentsMutation = + (typeof ShopperConsentsMutations)[keyof typeof ShopperConsentsMutations] + +/** + * Mutation hook for Shopper Consents. + * @group ShopperConsents + * @category Mutation + */ +export function useShopperConsentsMutation( + mutation: Mutation +): UseMutationResult, unknown, Argument> { + 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 + type Data = DataType + return useMutation({ + client, + method: (opts: Options) => (client[mutation] as ApiMethod)(opts), + getCacheUpdates: getCacheUpdates as unknown as CacheUpdateGetter< + MergedOptions, + Data + > + }) +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperConsents/query.test.ts b/packages/commerce-sdk-react/src/hooks/ShopperConsents/query.test.ts new file mode 100644 index 0000000000..588cef455e --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperConsents/query.test.ts @@ -0,0 +1,71 @@ +/* + * 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 {ShopperConsentsTypes} from 'commerce-sdk-isomorphic' +import nock from 'nock' +import { + mockQueryEndpoint, + renderHookWithProviders, + waitAndExpectError, + waitAndExpectSuccess, + createQueryClient +} from '../../test-utils' +import * as queries from './query' + +jest.mock('../../auth/index.ts', () => { + const {default: mockAuth} = jest.requireActual('../../auth/index.ts') + mockAuth.prototype.ready = jest.fn().mockResolvedValue({access_token: 'access_token'}) + return mockAuth +}) +type Queries = typeof queries +const consentsEndpoint = '/shopper/shopper-consents/' +// Not all endpoints use all parameters, but unused parameters are safely discarded +const OPTIONS = {parameters: {siteId: 'RefArch'}} + +/** Map of query name to returned data type */ +type TestMap = {[K in keyof Queries]: NonNullable['data']>} +// This is an object rather than an array to more easily ensure we cover all hooks +const testMap: TestMap = { + // Type assertion so that we don't have to implement the full type + useSubscriptions: {data: []} as ShopperConsentsTypes.ConsentSubscriptionResponse +} +// Type assertion is necessary because `Object.entries` is limited +const testCases = Object.entries(testMap) as Array<[keyof TestMap, TestMap[keyof TestMap]]> +describe('Shopper Consents query hooks', () => { + beforeEach(() => nock.cleanAll()) + afterEach(() => { + expect(nock.pendingMocks()).toHaveLength(0) + }) + test.each(testCases)('`%s` returns data on success', async (queryName, data) => { + mockQueryEndpoint(consentsEndpoint, data) + const {result} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectSuccess(() => result.current) + expect(result.current.data).toEqual(data) + }) + + test.each(testCases)('`%s` has meta.displayName defined', async (queryName, data) => { + mockQueryEndpoint(consentsEndpoint, data) + const queryClient = createQueryClient() + const {result} = renderHookWithProviders( + () => { + return queries[queryName](OPTIONS) + }, + {queryClient} + ) + await waitAndExpectSuccess(() => result.current) + expect(queryClient.getQueryCache().getAll()[0].meta?.displayName).toBe(queryName) + }) + + test.each(testCases)('`%s` returns error on error', async (queryName) => { + mockQueryEndpoint(consentsEndpoint, {}, 400) + const {result} = renderHookWithProviders(() => { + return queries[queryName](OPTIONS) + }) + await waitAndExpectError(() => result.current) + }) +}) diff --git a/packages/commerce-sdk-react/src/hooks/ShopperConsents/query.ts b/packages/commerce-sdk-react/src/hooks/ShopperConsents/query.ts new file mode 100644 index 0000000000..0abb3ac154 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperConsents/query.ts @@ -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 {UseQueryResult} from '@tanstack/react-query' +import {ShopperConsents} from 'commerce-sdk-isomorphic' +import {ApiClients, ApiQueryOptions, Argument, DataType, NullableParameters} from '../types' +import {useQuery} from '../useQuery' +import {mergeOptions, omitNullableParameters, pickValidParams} from '../utils' +import * as queryKeyHelpers from './queryKeyHelpers' +import {CLIENT_KEYS} from '../../constant' +import useCommerceApi from '../useCommerceApi' + +const CLIENT_KEY = CLIENT_KEYS.SHOPPER_CONSENTS +type Client = NonNullable + +/** + * Gets the customer's consents for receiving marketing emails, SMS, etc. + * @group ShopperConsents + * @category Query + * @parameter apiOptions - Options to pass through to `commerce-sdk-isomorphic`, with `null` accepted for unset API parameters. + * @parameter queryOptions - TanStack Query query options, with `enabled` by default set to check that all required API parameters have been set. + * @returns A TanStack Query query hook with data from the Shopper Consents `getSubscriptions` endpoint. + * @see {@link https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-consents?meta=getSubscriptions| Salesforce Developer Center} for more information about the API endpoint. + * @see {@link https://salesforcecommercecloud.github.io/commerce-sdk-isomorphic/classes/shopperconsents.shopperconsents-1.html#getsubscriptions | `commerce-sdk-isomorphic` documentation} for more information on the parameters and returned data type. + * @see {@link https://tanstack.com/query/latest/docs/react/reference/useQuery | TanStack Query `useQuery` reference} for more information about the return value. + */ +export const useSubscriptions = ( + apiOptions: NullableParameters>, + queryOptions: ApiQueryOptions = {} +): UseQueryResult> => { + type Options = Argument + type Data = DataType + const client = useCommerceApi(CLIENT_KEY) + const methodName = 'getSubscriptions' + const requiredParameters = ShopperConsents.paramKeys[`${methodName}Required`] + + // Parameters can be set in `apiOptions` or `client.clientConfig`; + // we must merge them in order to generate the correct query key. + const netOptions = omitNullableParameters(mergeOptions(client, apiOptions)) + const parameters = pickValidParams(netOptions.parameters, ShopperConsents.paramKeys[methodName]) + const queryKey = queryKeyHelpers[methodName].queryKey(netOptions.parameters) + + // We don't use `netOptions` here because we manipulate the options in `useQuery`. + const method = async (options: Options) => await client[methodName](options) + + queryOptions.meta = { + displayName: 'useSubscriptions', + ...queryOptions.meta + } + + // For some reason, if we don't explicitly set these generic parameters, the inferred type for + // `Data` sometimes, but not always, includes `Response`, which is incorrect. I don't know why. + return useQuery({...netOptions, parameters}, queryOptions, { + method, + queryKey, + requiredParameters + }) +} diff --git a/packages/commerce-sdk-react/src/hooks/ShopperConsents/queryKeyHelpers.ts b/packages/commerce-sdk-react/src/hooks/ShopperConsents/queryKeyHelpers.ts new file mode 100644 index 0000000000..4e8e8bbde0 --- /dev/null +++ b/packages/commerce-sdk-react/src/hooks/ShopperConsents/queryKeyHelpers.ts @@ -0,0 +1,46 @@ +/* + * 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 {ShopperConsents} from 'commerce-sdk-isomorphic' +import {Argument, ExcludeTail} from '../types' +import {pickValidParams} from '../utils' + +// We must use a client with no parameters in order to have required/optional match the API spec +type Client = ShopperConsents<{shortCode: string}> +type Params = Partial['parameters']> +export type QueryKeys = { + getSubscriptions: [ + '/commerce-sdk-react', + '/organizations/', + string | undefined, + '/shopper-consents/subscriptions', + Params<'getSubscriptions'> + ] +} + +// This is defined here, rather than `types.ts`, because it relies on `Client` and `QueryKeys`, +// and making those generic would add too much complexity. +type QueryKeyHelper = { + /** Generates the path component of the query key for an endpoint. */ + path: (params: Params) => ExcludeTail + /** Generates the full query key for an endpoint. */ + queryKey: (params: Params) => QueryKeys[T] +} + +export const getSubscriptions: QueryKeyHelper<'getSubscriptions'> = { + path: (params) => [ + '/commerce-sdk-react', + '/organizations/', + params?.organizationId, + '/shopper-consents/subscriptions' + ], + queryKey: (params: Params<'getSubscriptions'>) => { + return [ + ...getSubscriptions.path(params), + pickValidParams(params || {}, ShopperConsents.paramKeys.getSubscriptions) + ] + } +} diff --git a/packages/commerce-sdk-react/src/hooks/index.ts b/packages/commerce-sdk-react/src/hooks/index.ts index c285391d0c..43cc7daaad 100644 --- a/packages/commerce-sdk-react/src/hooks/index.ts +++ b/packages/commerce-sdk-react/src/hooks/index.ts @@ -1,10 +1,11 @@ /* - * Copyright (c) 2024, Salesforce, Inc. + * 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 './ShopperBaskets' +export * from './ShopperConsents' export * from './ShopperContexts' export * from './ShopperCustomers' export * from './ShopperExperience' diff --git a/packages/commerce-sdk-react/src/hooks/types.ts b/packages/commerce-sdk-react/src/hooks/types.ts index 6390bd262f..a86ded5753 100644 --- a/packages/commerce-sdk-react/src/hooks/types.ts +++ b/packages/commerce-sdk-react/src/hooks/types.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Salesforce, Inc. + * 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 @@ -8,6 +8,7 @@ import {InvalidateQueryFilters, QueryFilters, Updater, UseQueryOptions} from '@t import { ShopperBaskets, ShopperConfigurations, + ShopperConsents, ShopperContexts, ShopperCustomers, ShopperExperience, @@ -86,6 +87,7 @@ export type ApiClientConfigParams = { */ export interface ApiClients { shopperBaskets?: ShopperBaskets + shopperConsents?: ShopperConsents shopperContexts?: ShopperContexts shopperCustomers?: ShopperCustomers shopperExperience?: ShopperExperience diff --git a/packages/commerce-sdk-react/src/provider.tsx b/packages/commerce-sdk-react/src/provider.tsx index 11605f6e8a..bb4ce39f1d 100644 --- a/packages/commerce-sdk-react/src/provider.tsx +++ b/packages/commerce-sdk-react/src/provider.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, salesforce.com, inc. + * 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 @@ -11,6 +11,7 @@ import {Logger} from './types' import {DWSID_COOKIE_NAME, SERVER_AFFINITY_HEADER_KEY} from './constant' import { ShopperBaskets, + ShopperConsents, ShopperContexts, ShopperConfigurations, ShopperCustomers, @@ -257,6 +258,7 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => { return { shopperBaskets: new ShopperBaskets(config), + shopperConsents: new ShopperConsents(config), shopperContexts: new ShopperContexts(config), shopperConfigurations: new ShopperConfigurations(config), shopperCustomers: new ShopperCustomers(config), diff --git a/packages/pwa-kit-runtime/package-lock.json b/packages/pwa-kit-runtime/package-lock.json index 0d8ea1fc57..1a967a2219 100644 --- a/packages/pwa-kit-runtime/package-lock.json +++ b/packages/pwa-kit-runtime/package-lock.json @@ -41,7 +41,7 @@ "npm": "^9.0.0 || ^10.0.0 || ^11.0.0" }, "peerDependencies": { - "@salesforce/pwa-kit-dev": "3.16.0-dev" + "@salesforce/pwa-kit-dev": "3.17.0-dev" }, "peerDependenciesMeta": { "@salesforce/pwa-kit-dev": { @@ -1482,6 +1482,7 @@ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -1522,6 +1523,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" } @@ -1531,6 +1533,7 @@ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", @@ -1547,6 +1550,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", @@ -1563,6 +1567,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" } @@ -1572,6 +1577,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -1581,6 +1587,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "license": "MIT", + "peer": true, "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" @@ -1594,6 +1601,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", @@ -1620,6 +1628,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -1638,6 +1647,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -1647,6 +1657,7 @@ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "license": "MIT", + "peer": true, "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" @@ -1660,6 +1671,7 @@ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.28.5" }, @@ -1697,6 +1709,7 @@ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", @@ -1711,6 +1724,7 @@ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1729,6 +1743,7 @@ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" @@ -1832,6 +1847,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" @@ -1842,6 +1858,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -1852,6 +1869,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.0.0" } @@ -1860,13 +1878,15 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2594,6 +2614,7 @@ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -2706,6 +2727,7 @@ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.12.tgz", "integrity": "sha512-Mij6Lij93pTAIsSYy5cyBQ975Qh9uLEc5rwGTpomiZeXZL9yIS6uORJakb3ScHgfs0serMMfIbXzokPMuEiRyw==", "license": "Apache-2.0", + "peer": true, "bin": { "baseline-browser-mapping": "dist/cli.js" } @@ -2746,7 +2768,6 @@ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", - "peer": true, "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", @@ -2909,7 +2930,8 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "CC-BY-4.0" + "license": "CC-BY-4.0", + "peer": true }, "node_modules/chokidar": { "version": "3.6.0", @@ -2991,7 +3013,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cookie": { "version": "0.7.2", @@ -3161,7 +3184,8 @@ "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/encodeurl": { "version": "2.0.0", @@ -3232,6 +3256,7 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -3262,7 +3287,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -3512,6 +3536,7 @@ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -3660,7 +3685,6 @@ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", - "peer": true, "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", @@ -3861,6 +3885,7 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", + "peer": true, "bin": { "jsesc": "bin/jsesc" }, @@ -3886,6 +3911,7 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", + "peer": true, "bin": { "json5": "lib/cli.js" }, @@ -3945,6 +3971,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "license": "ISC", + "peer": true, "dependencies": { "yallist": "^3.0.2" } @@ -4196,7 +4223,8 @@ "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/nodemon": { "version": "2.0.22", @@ -5085,6 +5113,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -5164,7 +5193,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" + "license": "ISC", + "peer": true } } } diff --git a/packages/template-retail-react-app/CHANGELOG.md b/packages/template-retail-react-app/CHANGELOG.md index 1f24aa2509..db6b8e30a0 100644 --- a/packages/template-retail-react-app/CHANGELOG.md +++ b/packages/template-retail-react-app/CHANGELOG.md @@ -1,5 +1,6 @@ ## v9.1.0-dev - Add Node 24 support. Drop Node 16 support [#3652](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3652) +- [Feature] Subscribe to marketing communications. Email capture component updated in footer section to use Shopper Consents API. [#3674](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3674) ## v9.0.0 (Feb 12, 2026) - [Feature] One Click Checkout (in Developer Preview) [#3552](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3552) diff --git a/packages/template-retail-react-app/app/components/footer/index.jsx b/packages/template-retail-react-app/app/components/footer/index.jsx index 9500aad913..daebaa5d3d 100644 --- a/packages/template-retail-react-app/app/components/footer/index.jsx +++ b/packages/template-retail-react-app/app/components/footer/index.jsx @@ -13,27 +13,23 @@ import { SimpleGrid, useMultiStyleConfig, Select as ChakraSelect, - Heading, - Input, - InputGroup, - InputRightElement, createStylesContext, - Button, FormControl } from '@salesforce/retail-react-app/app/components/shared/ui' import {useIntl} from 'react-intl' import LinksList from '@salesforce/retail-react-app/app/components/links-list' -import SocialIcons from '@salesforce/retail-react-app/app/components/social-icons' +import SubscribeMarketingConsent from '@salesforce/retail-react-app/app/components/subscription' import {HideOnDesktop, HideOnMobile} from '@salesforce/retail-react-app/app/components/responsive' import {getPathWithLocale} from '@salesforce/retail-react-app/app/utils/url' import LocaleText from '@salesforce/retail-react-app/app/components/locale-text' import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site' import styled from '@emotion/styled' import {STORE_LOCATOR_IS_ENABLED} from '@salesforce/retail-react-app/app/constants' +import {CONSENT_TAGS} from '@salesforce/retail-react-app/app/constants/marketing-consent' import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config' -const [StylesProvider, useStyles] = createStylesContext('Footer') +const [StylesProvider] = createStylesContext('Footer') const Footer = ({...otherProps}) => { const styles = useMultiStyleConfig('Footer') const intl = useIntl() @@ -129,13 +125,13 @@ const Footer = ({...otherProps}) => { links={makeOurCompanyLinks()} /> - + - + {showLocaleSelector && ( @@ -202,56 +198,6 @@ const Footer = ({...otherProps}) => { export default Footer -const Subscribe = ({...otherProps}) => { - const styles = useStyles() - const intl = useIntl() - return ( - - - {intl.formatMessage({ - id: 'footer.subscribe.heading.first_to_know', - defaultMessage: 'Be the first to know' - })} - - - {intl.formatMessage({ - id: 'footer.subscribe.description.sign_up', - defaultMessage: 'Sign up to stay in the loop about the hottest deals' - })} - - - - - {/* Had to swap the following InputRightElement and Input - to avoid the hydration error due to mismatched html between server and client side. - This is a workaround for Lastpass plugin that automatically injects its icon for input fields. - */} - - - - - - - - - - ) -} - const LegalLinks = ({variant}) => { const intl = useIntl() return ( diff --git a/packages/template-retail-react-app/app/components/footer/index.test.js b/packages/template-retail-react-app/app/components/footer/index.test.js index 335656bcbf..8fc63383ec 100644 --- a/packages/template-retail-react-app/app/components/footer/index.test.js +++ b/packages/template-retail-react-app/app/components/footer/index.test.js @@ -12,7 +12,8 @@ import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-u test('renders component', () => { renderWithProviders(