> This page is for Developer.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.bigcommerce.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.bigcommerce.com/_mcp/server.

# Lab - Simulate a CRM Integration

Plan: B2B Developer

Lesson 22 of 25 · 30 min

## Introduction

This exercise will simulate an external CRM integration by fetching support case data from an API and displaying it in the Buyer Portal.

### In this lab, you will:

* Perform a mock token exchange with the CRM
* Make a mock data fetch for support case data
* Display the support case status on the Overview page

## Setup

This exercise continues where you left off previously. Make sure the dev server is started in your Buyer Portal project:

```shell showLineNumbers={false}
yarn dev
```

If you need a fresh start, you can follow the instructions below to set up a new project complete with previous exercise code.

1. **Copy** the project with the appropriate tag.

```shell showLineNumbers={false}
degit https://github.com/bigcommerce-edu/lab-b2b-buyer-portal.git#e-crm-pre <directory-name>
```

2. **Install** dependencies.

```shell showLineNumbers={false}
cd <directory-name>
yarn install
```

3. **Run** the dev server.

```shell showLineNumbers={false}
yarn dev
```

> **Info**
>
> **Remember!**
>
> Script Manager in your store control panel must contain appropriate header/footer scripts to load the Buyer Portal from your local environment on the appropriate port.
>
> Revisit the initial setup lab for full details on setting up your environment.

### Exercise Reference Copy

If it's helpful to compare your own code as you go, you can clone or download the completed version of this labs as a reference.

[CRM Lab Snapshot](https://github.com/bigcommerce-edu/lab-b2b-buyer-portal/tree/e-crm-post)

## The Mock CRM Service

The lab boilerplate code already includes a minimal implementation of a mock CRM service. [See the implementation here](https://github.com/bigcommerce-edu/lab-b2b-buyer-portal/compare/e-bp-mock-client-pre...e-bp-mock-client-post?diff=split).

This implementation includes:

* A mock client simulating fetches to these REST endpoints:
  * `/storefrontToken`
  * `/company/orderCases`
* Service methods `fetchStorefrontToken` and `fetchOrderSupportCases` that wrap the mock client calls.

The client simulates a workflow for integrating with a hypothetical "backend for frontend" (BFF) service - a thin middleware layer proxying data from the CRM to the Buyer Portal.

Important notes about the mock implementation:

* The mock client simulates a random delay of 1-5 seconds for each request.
* `fetchStorefrontToken` simulates a token exchange. In a real-world implementation, the BFF service would verify that the BigCommerce B2B storefront token is valid and matches a given company in the CRM, then return a signed JWT with a unique token and CRM ID for the company. In this mock implementation, the request automatically succeeds and returns a randomly generated token.
* `fetchOrderSupportCases` simulates a data fetch for support case data from the CRM, for whichever B2B orders are passed in. In a real-world implementation, the BFF service would store minimal support case data pushed to it from the CRM, as well as take care of validating the CRM JWT from the Buyer Portal. In this mock implementation, the token validation is done in the client itself, and a static array of case data is returned regardless of what B2B order IDs are passed in.

The following diagram shows the hypothetical request flow between the client (Buyer Portal), BFF service, BigCommerce B2B, and CRM:

```mermaid
sequenceDiagram
    participant Client
    participant BFF as BFF Service
    participant BC as BigCommerce B2B
    participant CRM

    Note over CRM,BFF: Push Support Case Data
    CRM->>BFF: Support case created for Order A
    CRM->>BFF: Support case updated for Order B

    Note over Client, CRM: Token Exchange
    Client->>BFF: Token exchange request (B2B token)
    BFF->>BC: Validate B2B token
    BC-->>BFF: Token valid
    BFF->>CRM: Validate company information
    CRM-->>BFF: Company valid
    BFF-->>Client: CRM token

    Note over Client, CRM: Fetch Support Cases
    Client->>BFF: Fetch support cases request (CRM token)
    BFF->>BFF: Validate token
    BFF-->>Client: Case data
```

## Managing the CRM Token

In a previous lab exercise, we added a new Redux slice allowing us to manage the CRM token in the Buyer Portal global state.

We added these entry points:

* When the Buyer Portal is initialized in `App.tsx`, a value is set in the global state.
* In the `RecentOrders` component, the CRM token is retrieved from the global state.

We'll be updating the initialization logic to perform a token exchange with the mock CRM service.

## Step 1: Perform Token Exchange

> **Info**
>
> Remember, all file paths referenced in these exercises are relative to `apps/storefront/src` in your Buyer Portal project.

1. **Open** `shared/service/crm-bff/initCrm.ts` and **implement** the logic to perform the token exchange with the mock client.

**`initCrm.ts`**

```tsx {8-16} showLineNumbers={false} title="initCrm.ts"
const initCrm = async ({
  b2bToken,
  companyId,
}: {
  b2bToken: string,
  companyId: string,
}) => {
  if (!b2bToken || !companyId) return;

  const token = await fetchStorefrontToken({
    b2bToken,
    b2bCompanyId: companyId,
    storeHash,
  });

  return token;
};
```

2. **Open** `App.tsx` and **update** imports to include `initCrm` and add the selector from the Redux store.

**`App.tsx`**

```tsx showLineNumbers={false} title="App.tsx"
import { selectCrmToken, setCrmToken } from '@/store';
import { initCrm } from '@/shared/service/crm-bff/initCrm';
```

3. Near the top of the `App` component, **add** the following to fetch the values we need from the global state.

**`App.tsx`**

```tsx {7-9} showLineNumbers={false} title="App.tsx"
export default function App() {
  ...

  const { quotesCreateActionsPermission, shoppingListCreateActionsPermission } =
    useAppSelector(...);

  const b2bToken = useAppSelector(({ company }) => company.tokens.B2BToken);
  const companyId = useAppSelector(({ company }) => company.companyInfo.id);
  const crmToken = useAppSelector(selectCrmToken);

  ...
}
```

> **Info**
>
> Note that the callbacks passed to `useAppSelector` for the built-in values from the `company` slice are defined inline. For our `crm` slice, however, we have exported an explicit selector for use in scenarios like this, meaning we don't have to be concerned with the details of the slice and its structure.

4. **Update** the existing React side effect that sets the CRM token in the global state.

**`App.tsx`**

```tsx {5-15} showLineNumbers={false} title="App.tsx"
export default function App() {
  ...

  useEffect(() => {
    if (b2bToken && companyId && !crmToken) {
      initCrm({
        b2bToken,
        companyId,
      }).then((token) => {
        if (token) {
          storeDispatch(setCrmToken(token));
        }
      });
    }
  }, [storeDispatch, b2bToken, companyId, crmToken]);

  return (
    ...
  );
}
```

> **Warning**
>
> **Important!**
>
> Our logic here only performs the token exchange if a token isn't already present in the session. Due to the test value already stored in the previous lab exercise, you will likely need to use your browser developer tools to delete the old value before you can see the token exchange in action.
>
> In your session storage, look for the key "persist:crm" and delete the value.

**You should now be able to observe the effects of the token exchange in your browser developer tools, when reloading the Overview page:**

* **The console log will show request and response details for the mock client request.**
* **As before, you'll see a console log of the value retrieved from global state and the fresh `persist:crm` value in session storage.**

Observe that the mock client request will occur on any Buyer Portal page, and also that this request won't be repeated once the value is stored in session storage.

> **Info**
>
> [**View the Code**](https://github.com/bigcommerce-edu/lab-b2b-buyer-portal/compare/e-crm-01-pre...e-crm-01-post?diff=split)

## Step 2: Fetch Support Case Data

In this step, we'll use the mock client and the user's CRM token to fetch support case data for the recent orders.

1. **Open** `pages/Overview/components/RecentOrders.tsx` and **add** a React state value to track the "B2B orders" separately from the main orders array.

**`RecentOrders.tsx`**

```tsx {6, 15} showLineNumbers={false} title="RecentOrders.tsx"
export default function RecentOrders({
  ...
}: OrdersProps) {
  ...

  const [b2bOrders, setB2bOrders] = useState<OverviewOrder[]>([]);
  const [orders, setOrders] = useState<OverviewOrder[]>([]);

  ...

  useEffect(() => {
    if (!startLoad || !loading) return;

    getRecentOrders().then((b2bOrders) => {
      setB2bOrders(b2bOrders);
      setOrders(b2bOrders);
      setLoading(false);
    });
  }, [startLoad, loading]);

  useEffect(() => {
    if (!crmToken) return;

    ...
  }, [crmToken]);

  ...
}
```

Note we're now setting the identical array of B2B orders in both the `orders` and `b2bOrders` state values. The former populates the rendered table and allows this rendering to proceed without being blocked by the CRM fetch. The latter allows us to construct a side effect to load the CRM case data.

2. **Update** the side effect that currently simply logs the CRM token.

**`RecentOrders.tsx`**

```tsx {13-24} showLineNumbers={false} title="RecentOrders.tsx"
export default function RecentOrders({
  ...
}: OrdersProps) {
  ...

  useEffect(() => {
    if (!startLoad || !loading) return;

    ...
  }, [startLoad, loading]);

  useEffect(() => {
    if (!crmToken || (b2bOrders.length <= 0)) return;

    crmFetchSupportCases({
      token: crmToken,
      filters: {
        b2bOrderIds: b2bOrders.map((order) => order.orderId),
      },
    }).then((crmCases) => {
      // TODO: Remove this console.log after implementing the main logic
      console.log(crmCases);
    });
  }, [crmToken, b2bOrders]);

  const orderColumns = [
    ...
  ];

  ...
}
```

As soon as the main B2B order records have been loaded, `crmFetchSupportCases` will now be called to fetch the support case data (including a `status`). The fetched data is simply being logged for now.

Note that the side effect now depends on `b2bOrders` being non-empty.

**By reloading the Overview page and expanding the "Recent Orders" accordion, you should now be able to observe the fetch. Watch the console in your browser developer tools to see both the mock client request/response and the array logged by `RecentOrders`.**

> **Info**
>
> [**View the Code**](https://github.com/bigcommerce-edu/lab-b2b-buyer-portal/compare/e-crm-02-pre...e-crm-02-post?diff=split)

## Step 3: Display the Support Case Status

In the process of adding the CRM-fetched support case status to the orders rendered on the page, we'll have to expand the interfaces that define an order's fields.

1. **Open** `pages/Overview/components/RecentOrders.tsx`, **add** a new interface that extends `OverviewOrder`, and **update** the `orders` state accordingly.

**`RecentOrders.tsx`**

```tsx {5-7, 14} showLineNumbers={false} title="RecentOrders.tsx"
interface OrdersProps {
  ...
}

interface OverviewOrderWithSupportCaseStatus extends OverviewOrder {
  supportCaseStatus?: string;
}

export default function RecentOrders({
  ...
}: OrdersProps) {
  ...

  const [orders, setOrders] = useState<OverviewOrderWithSupportCaseStatus[]>([]);

  ...
}
```

Note that the `b2bOrders` state is still an array of `OverviewOrder`, since it only ever contains the un-enhanced B2B orders.

2. **Update** the type information for column definitions with `render` callbacks.

**`RecentOrders.tsx`**

```tsx {11, 18} showLineNumbers={false} title="RecentOrders.tsx"
export default function RecentOrders({
  ...
}: OrdersProps) {
  ...

  const orderColumns = [
    ...
    {
      key: 'totalIncTax',
      title: b3Lang('orders.grandTotal'),
      render: (item: OverviewOrderWithSupportCaseStatus) => {
        return currencyFormat(item.totalIncTax);
      },
    },
    {
      key: 'createdAt',
      title: b3Lang('orders.createdOn'),
      render: (item: OverviewOrderWithSupportCaseStatus) => {
        return `${displayFormat(Number(item.createdAt))}`;
      },
    },
  ];

  return (
    ...
  );
}
```

3. **Update** the side effect that makes the CRM fetch.

**`RecentOrders.tsx`**

```tsx {12-19} showLineNumbers={false} title="RecentOrders.tsx"
export default function RecentOrders({
  ...
}: OrdersProps) {
  ...

  useEffect(() => {
    if (!crmToken || (b2bOrders.length <= 0)) return;

    crmFetchSupportCases({
      ...
    }).then((crmCases) => {
      const updatedOrders = b2bOrders.map((order) => {
        const crmCase = crmCases.find((crmCase) => crmCase.b2bOrderId === order.orderId);
        return {
          ...order,
          supportCaseStatus: crmCase?.status,
        };
      });
      setOrders(updatedOrders);
    });
  }, [crmToken, b2bOrders]);

  ...
}
```

`updatedOrders` is set by looping over `b2bOrders`, finding the matching CRM case, and adding the `supportCaseStatus` to the final object. This is then set on the `orders` state value that populates the rendered table.

4. **Add** a column value to render the support case status.

**`RecentOrders.tsx`**

```tsx {8-17} showLineNumbers={false} title="RecentOrders.tsx"
export default function RecentOrders({
  ...
}: OrdersProps) {
  ...

  const orderColumns = [
    ...
    {
      key: 'supportCaseStatus',
      title: b3Lang('overview.orders.supportCaseStatus'),
      render: (item: OverviewOrderWithSupportCaseStatus) => {
        if (!item.supportCaseStatus) {
          return <CircularProgress size={16} />;
        }
        return item.supportCaseStatus;
      },
    },
  ];

  ...
}
```

**The "Recent Orders" table should now operate as follows:**

* **The initial fetch with the B2B GraphQL API will occur when the accordion is expanded.**
* **The table will be rendered immediately when the B2B orders are available, with a loading spinner in the Support case status column.**
* **The CRM fetch will be triggered as soon as B2B orders are available, and the Support case status column will be populated with the CRM case status as soon as the response is received.**

![Recent Orders with CRM fetch](/_fern-files/bigcommerce.docs.buildwithfern.com/982ffd0e50ba7ff98d871e7eba3cabba565dd96ddc5563c69f4ad4d1775ae5e8/assets/learn/b2b-buyer-portal/recent-orders-case.gif)

> **Info**
>
> [**View the Code**](https://github.com/bigcommerce-edu/lab-b2b-buyer-portal/compare/e-crm-03-pre...e-crm-03-post?diff=split)

## Full Exercise Code

> **Info**
>
> [**Full Exercise Code**](https://github.com/bigcommerce-edu/lab-b2b-buyer-portal/compare/e-crm-pre...e-crm-post?diff=split)