Implement Hybrid Authentication for PWA Kit v2

Who should use this guide: Only existing PWA Kit v2.x projects that want to add Hybrid Auth by upgrading to v2.10.0. PWA Kit v2.10.0 supports SLAS Public Clients only.

Starting a new Hybrid Auth implementation? Don’t use this guide. New implementations must start on PWA Kit v3—see Configure a Hybrid Storefront with Hybrid Auth (PWA Kit) for the supported version and prerequisites.

Important

Use this guide to upgrade your existing PWA Kit application to version 2.10.0 and implement Hybrid Auth. The PWA Kit 2.10.0 release introduces significant changes, such as compatibility with Hybrid Auth and integration with the @salesforce/commerce-sdk-react package. For additional details, check out the complete pwa-kit GitHub diff to get a better understanding of the code changes.

Prerequisites 

Before starting the upgrade process, make sure that you have:

  • Node.js version 18.x or 20.x (upgraded from 14.x/16.x support)
  • NPM version 9.x or 10.x (upgraded from 6.x/7.x/8.x support)
  • Enable Hybrid Auth on your Business Manager instance. For details, see Shared Hybrid Auth Setup. Then enable cookies on Managed Runtime, as described in Enable Cookies on Managed Runtime.

Authentication System Updates 

The most significant update is the introduction of the new Commerce SDK React authentication system.

New Dependencies 

1{
2  "@salesforce/commerce-sdk-react": "^4.0.0",
3  "@tanstack/react-query": "4.28.0"
4}

Step-by-Step Upgrade Process 

Step 1: Prepare Your Environment 

  1. Check your current Node.js and npm versions.

    1node --version
    2npm --version

    If your Node.js version is below 18.x, upgrade it first. If your npm version is below 9.x, upgrade it first.

    Before the upgrade (PWA Kit v2.9.x): 

    1{
    2  "engines": {
    3    "node": "^14.0.0 || ^16.0.0 || ^18.0.0 || ^20.0.0",
    4    "npm": "^6.14.4 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0"
    5  }
    6}

    After the upgrade (PWA Kit v2.10.0): 

    1{
    2  "engines": {
    3    "node": "^18.0.0 || ^20.0.0",
    4    "npm": "^9.0.0 || ^10.0.0"
    5  }
    6}

Step 2: Update Dependencies 

  1. Open your package.json file and update the following sections.

    Update the "engines" section.

    1{
    2  "engines": {
    3    "node": "^18.0.0 || ^20.0.0",
    4    "npm": "^9.0.0 || ^10.0.0"
    5  }
    6}

    Update the "devDependencies" section with these new dependencies and version bumps.

    1{
    2  "devDependencies": {
    3    "@salesforce/commerce-sdk-react": "^3.4.0",
    4    "@tanstack/react-query": "4.28.0",
    5    "commerce-sdk-isomorphic": "^3.3.0",
    6    "jwt-decode": "^4.0.0",
    7    "pwa-kit-dev": "2.10.0",
    8    "pwa-kit-react-sdk": "2.10.0",
    9    "pwa-kit-runtime": "2.10.0",
    10    "@testing-library/react": "^12.1.5",
    11    "@testing-library/react-hooks": "^8.0.1",
    12    "@testing-library/user-event": "^14.4.3",
    13    "ajv": "^8.17.1",
    14    "ajv-keywords": "^5.1.0"
    15  }
    16}
  2. Save the package.json file.

  3. Clean install the new dependencies.

    1rm -rf node_modules package-lock.json
    2npm install

The @testing-library/* dependencies are optional and required only if you’ve implemented unit tests using react-testing-library.

Note

Step 3: Update the Configuration 

  1. Open your config/default.js file.

  2. Find the line that contains ssrFunctionNodeVersion and update it.

    1// Change this line:
    2ssrFunctionNodeVersion: '18.x', // Or 20.x if you upgraded to Node 20.
  3. Save the config/default.js file.

Step 4: Authentication Migration 

Update your authentication implementation with the new Commerce SDK React system.

  1. Remove old authentication files.

    1rm app/commerce-api/auth.js
    2rm app/commerce-api/pkce.js
  2. Update the utils.js file.

    1# Open app/commerce-api/utils.js and remove authentication-related functions

    Remove these imports from the top of the file.

    1// REMOVE these lines from app/commerce-api/utils.js
    2import jwtDecode from "jwt-decode";
    3import { refreshTokenGuestStorageKey, refreshTokenRegisteredStorageKey } from "./constants";

    Remove these functions from app/commerce-api/utils.js.

    1// REMOVE these functions from app/commerce-api/utils.js
    2export function isTokenExpired(token) { ... }
    3export function createGetTokenBody(urlString, slasCallbackEndpoint, codeVerifier) { ... }
    4export function hasSFRAAuthStateChanged(storage, storageCopy) { ... }

    Keep all other utility functions, including keysToCamel and createOcapiFetch, as they’re still needed.

  3. Update the useCustomer hook. Open your app/hooks/use-customer.js file and update the authentication methods.

    1// Add these new imports at the top of the file:
    2import { AuthHelpers } from '@salesforce/commerce-sdk-react'
    3import { useAuthHelper } from '@salesforce/commerce-sdk-react'
    4
    5// Find the login and logout methods in useCustomer hook and update them:
    6
    7// Old implementation (v2.9.x)
    8const login = async (credentials) => {
    9  const auth = new Auth();
    10  await auth.login(credentials);
    11  // ... rest of login logic
    12};
    13
    14const logout = async () => {
    15  const auth = new Auth();
    16  await auth.logout();
    17  // ... rest of logout logic
    18};
    19
    20// New implementation (v2.10.0)
    21export default function useCustomer() {
    22 const api = useCommerceAPI()
    23 const {customer, setCustomer} = useContext(CustomerContext)
    24
    25 const login = useAuthHelper(AuthHelpers.LoginRegisteredUserB2C)
    26 const logout = useAuthHelper(AuthHelpers.Logout)
    27
    28   const getSkeletonCustomer = () => {
    29       return {
    30           customerId: api.auth.get('customer_id'),
    31           authType: api.auth.get('customer_type')
    32       }
    33   }
    34
    35 const self = useMemo(() => {
    36   return {
    37     ... customer,
    38
    39     // Other useCustomer hook functions
    40     ...
    41     // The login method is now handled by the useAuthHelper hook
    42     async login(credentials) {
    43         await api.auth.ready()
    44         let skeletonCustomer = getSkeletonCustomer()
    45         if (credentials) {
    46             await login.mutateAsync({
    47                 username: credentials.email,
    48                 password: credentials.password
    49             })
    50             skeletonCustomer = getSkeletonCustomer()
    51         }
    52       // ... rest of login logic
    53       },
    54
    55       async logout() {
    56         await logout.mutateAsync()
    57         await api.auth.ready()
    58         const skeletonCustomer = getSkeletonCustomer()
    59         setCustomer(skeletonCustomer)
    60         // ... rest of logout logic
    61       }
    62   }
    63 })

The useCustomer hook now uses the new Commerce SDK React authentication methods. Your existing login and registration components don’t need any changes since they call the same login() and register() functions.

Note

Step 5: Update the Commerce API Implementation 

The Commerce API implementation has been significantly updated to use the new transformSDKClient utility from @salesforce/commerce-sdk-react package. See the README.md for the package to learn more.

Note

  1. Open your app/commerce-api/index.js file.

  2. Update the imports at the top of the file.

    1// Remove these old imports:
    2import Auth from "./auth";
    3import { isError } from "./utils";
    4
    5// Add these new imports:
    6import Auth from "@salesforce/commerce-sdk-react/auth";
    7import { transformSDKClient } from "@salesforce/commerce-sdk-react/utils";
    8import { DWSID_HEADER_KEY } from "./constants";
  3. Update the auth module instantiation.

    1// Old implementation (v2.9.x)
    2
    3this._config = { proxy, ...restConfig };
    4
    5this.auth = new Auth(this); // Update this with new authConfig

    Replace with this new code:

    1// Old implementation (v2.9.x)
    2
    3this._config = { proxy, ...restConfig };
    4
    5// Add new authConfig object
    6this._authConfig = {
    7  redirectURI: `${getAppOrigin()}/callback`,
    8  proxy,
    9  locale: this._config.locale,
    10  currency: this._config.currency,
    11  ...this._config.parameters,
    12  ...this._config.headers,
    13};
    14
    15this.auth = new Auth(this._authConfig); // pass authConfig as param to Auth class constructor.
  4. Find the section where SDK clients are instantiated (around line 100-150) and replace the entire SDK instantiation logic. Remove this old code:

    1// Old implementation (v2.9.x) - using Proxy
    2const SdkClass = apiConfigs[key].api;
    3self._sdkInstances = {
    4  ...self._sdkInstances,
    5  [key]: new Proxy(new SdkClass(this._config), {
    6    get: function (obj, prop) {
    7      // ... proxy implementation
    8    },
    9  }),
    10};

    Replace with this new code:

    1// New implementation (v2.10.0) - using transformSDKClient
    2const SdkClass = apiConfigs[key].api;
    3const sdkClient = new SdkClass(this._config);
    4self._sdkInstances = {
    5  ...self._sdkInstances,
    6  [key]: transformSDKClient(sdkClient, {
    7    props: this._config,
    8    transformer: async (_, methodName, options) => {
    9      const { fetchOptions = {} } = options;
    10      if (fetchOptions.ignoreHooks) {
    11        return options;
    12      }
    13
    14      const { locale, currency } = this._config;
    15
    16      // Inject the locale and currency to the API call via its parameters.
    17      const { sendLocale = true, sendCurrency = false } = apiConfigs[key];
    18
    19      const includeGlobalLocale = Array.isArray(sendLocale)
    20        ? sendLocale.includes(methodName)
    21        : !!sendLocale;
    22
    23      const includeGlobalCurrency = Array.isArray(sendCurrency)
    24        ? sendCurrency.includes(methodName)
    25        : !!sendCurrency;
    26
    27      fetchOptions["parameters"] = {
    28        ...(includeGlobalLocale ? { locale } : {}),
    29        ...(includeGlobalCurrency ? { currency } : {}),
    30        ...fetchOptions?.parameters,
    31      };
    32
    33      // Handle auth logic (replacing willSendRequest functionality)
    34      let dwsidHeader = {};
    35      const dwsid = self.auth.get("dwsid");
    36      if (dwsid) {
    37        dwsidHeader = {
    38          [DWSID_HEADER_KEY]: dwsid,
    39        };
    40      }
    41
    42      // Special handling for auth methods
    43      if (
    44        methodName === "authenticateCustomer" ||
    45        methodName === "authorizeCustomer" ||
    46        methodName === "getAccessToken"
    47      ) {
    48        return {
    49          ...options.parameters,
    50          headers: {
    51            ...options.headers,
    52            ...fetchOptions.headers,
    53          },
    54          credentials: "same-origin", // Required for SLAS calls to set dwsid cookie
    55          ...fetchOptions,
    56        };
    57      }
    58
    59      const { access_token: token } = await self.auth.ready();
    60      return {
    61        ...options,
    62        headers: {
    63          ...options.headers,
    64          ...dwsidHeader,
    65          Authorization: `Bearer ${token}`,
    66        },
    67        // Add cache breaker for Storefront Preview
    68        parameters: {
    69          ...options.parameters,
    70          ...(this.isStorefrontPreview ? { c_cache_breaker: Date.now() } : {}),
    71        },
    72      };
    73    },
    74  }),
    75};
  5. Find and remove these methods from the CommerceAPI class:

    1// Remove these methods completely:
    2async willSendRequest(methodName, ...params) {
    3    // ... entire method
    4}
    5
    6didReceiveResponse(response, args) {
    7    // ... entire method
    8}
  6. Update the constants file. Add a new constant in app/commerce-api/constants.js.

    1export const DWSID_HEADER_KEY = "sfdc_dwsid";

Step 6: Update Commerce API Contexts 

The Commerce API contexts have been updated to integrate with Commerce SDK React and React Query.

Note

  1. Open your app/commerce-api/contexts.js file.

  2. Add these new imports at the top.

    1import { CommerceApiProvider as CommerceSDKReactProvider } from "@salesforce/commerce-sdk-react";
    2import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
    3import { getAppOrigin } from "pwa-kit-react-sdk/utils/url";
    4import { isServer } from "../../pwa-kit-managed-runtime/utils/utils";
  3. Add the QueryClient configuration after the imports.

    1const queryClientOptions = {
    2  queryClientConfig: {
    3    defaultOptions: {
    4      queries: {
    5        retry: false,
    6        refetchOnWindowFocus: false,
    7        staleTime: 10 * 1000,
    8        ...(isServer ? { retryOnMount: false } : {}),
    9      },
    10      mutations: {
    11        retry: false,
    12      },
    13    },
    14  },
    15  beforeHydrate: (data) => {
    16    const now = Date.now();
    17    const updateQueryTimeStamp = ({ state }) => {
    18      state.dataUpdatedAt = now;
    19    };
    20    data?.mutations?.forEach(updateQueryTimeStamp);
    21    data?.queries?.forEach(updateQueryTimeStamp);
    22    return data;
    23  },
    24};
    25
    26const queryClient = new QueryClient(queryClientOptions);
  4. Replace the existing CommerceAPIProvider with this new implementation.

    1export const CommerceAPIProvider = ({ value, children }) => {
    2  const { api, site, locale } = value;
    3  const apiClients = api._sdkInstances;
    4
    5  const { shortCode, clientId, organizationId } = api.getConfig().parameters;
    6  const { proxy } = api.getConfig();
    7
    8  return (
    9    <CommerceAPIContext.Provider value={api}>
    10      <QueryClientProvider client={queryClient}>
    11        <CommerceSDKReactProvider
    12          shortCode={shortCode}
    13          clientId={clientId}
    14          organizationId={organizationId}
    15          siteId={site?.id}
    16          locale={locale?.id}
    17          currency={locale?.preferredCurrency}
    18          redirectURI={`${getAppOrigin()}/callback`}
    19          proxy={proxy}
    20          apiClients={apiClients}
    21          disableAuthInit={true}
    22        >
    23          {children}
    24        </CommerceSDKReactProvider>
    25      </QueryClientProvider>
    26    </CommerceAPIContext.Provider>
    27  );
    28};
  5. Save the app/commerce-api/contexts.js file.

Step 7: Update the App Configuration Provider 

Critical Change: The CommerceAPIProvider now requires different props structure.

Important

  1. Open your app/components/_app-config/index.jsx file.

  2. Find the CommerceAPIProvider component and update it.

    Replace this code:

    1// Old implementation (v2.9.x)
    2<CommerceAPIProvider value={locals.api}>

    With this code:

    1// New implementation (v2.10.0)
    2<CommerceAPIProvider value={locals}>
  3. Save the app/components/_app-config/index.jsx file.

Step 8: Cleanup Auth Initialization 

  1. Open your app/components/_app/index.jsx file.

  2. Remove the login call.

    1// Auth initialization is now handled by calling await self.auth.ready() in commerce-api/index.js
    2// Remove this line.
    3await api.auth.login();

Step 9: Test Your Upgrade 

  1. Start the development server.

    1npm start
  2. Run the build to check for errors.

    1npm run build

(Optional) Use Commerce SDK React Query Hooks 

With PWA Kit v2.10.0, you now have access to powerful query hooks from @salesforce/commerce-sdk-react that provide automatic caching, loading states, and error handling for both hybrid and non-hybrid PWA Kit v2.x storefronts. Here’s how to use them in your pages.

Example: Simple Order List Component 

Create a minimal order list component using the new query hooks.

1import React from "react";
2import { useCustomerOrders } from "@salesforce/commerce-sdk-react";
3import useCustomer from "../../commerce-api/hooks/useCustomer";
4
5const SimpleOrderList = () => {
6  const customer = useCustomer();
7
8  const { data: { data: orders } = {}, isLoading } = useCustomerOrders(
9    { parameters: { customerId: customer?.customerId } },
10    { enabled: !!customer?.customerId },
11  );
12
13  if (isLoading) return <div>Loading orders...</div>;
14
15  return (
16    <div>
17      <h2>My Orders</h2>
18      {orders?.map((order) => (
19        <div key={order.orderNo}>
20          <p>
21            Order #{order.orderNo} - {order.status}
22          </p>
23          <p>Total: ${order.orderTotal}</p>
24        </div>
25      ))}
26    </div>
27  );
28};
29
30export default SimpleOrderList;

Key Benefits 

Check out the key benefits of the new query hooks.

  • Automatic caching—Data is cached and shared.
  • Loading states—Built-in loading management.
  • Error handling—Automatic error states.
  • Optimized fetching—Only fetches when customer is available.
  • Simplified code—Less manual state management.

Additional Resources