# Implement the Customer module
Read Customer state from the InStore State module, and render a Customer lookup that replaces the one built into the InStore POS.
This page covers the interface that a Customer replacement module implements. For the build setup that every replacement module needs, see [Build a replacement module](/instore/customization/pos-module-development.md).
A Customer replacement module reads everything it needs from the Customer module context instead of calling the InStore backend.
This interface covers two external module types. Register `Customer` for the full-page lookup and `LookupCustomer` for the Customer control in the POS header. Both types read the same context. For the way one replacement module provides several components, see [Build a replacement module](/instore/customization/pos-module-development.md#configure-module-federation).
## Read Customer state and operations
`instoreState/context/CustomerModuleContext` provides `useCustomerModuleContext`, which returns the attached Customer and the operations that act on it.
`useCustomerModuleContext` returns a context object with the following members. They are marked `@public` in the types that the InStore State module publishes, and are supported for use in replacement modules. To resolve those types, see [Build a replacement module](/instore/customization/pos-module-development.md#resolve-types).
| Member | Description |
| --- | --- |
| `customer` | Customer attached to the active Cart, or `null` when none is attached. |
| `removeCustomer` | Detaches the Customer from the active Cart. |
| `searchByEmailAndAttach` | Looks a Customer up by exact email, then attaches it to the active Cart. |
| `searchCustomer` | Searches Customers by offset and limit. |
The provider adds the location and workstation to every search, so your module does not pass them.
Both `searchByEmailAndAttach` and `searchCustomer` require Customer Search to be active and its index to be ready on the commercetools Project. Neither has a fallback while Customer Search is inactive or still indexing: `searchByEmailAndAttach` resolves to `search-failed`, and `searchCustomer` rejects, the same as any other backend failure.
### Paginate a search
`searchCustomer` takes a request of the following shape, omitting `locationKey` and `workstationKey`, which the provider fills in for you:
```ts
{
query: SearchQuery;
sort?: SearchSorting[];
limit?: number;
offset?: number;
}
```
It resolves to a page of the following shape:
```ts
{
limit: number;
offset: number;
count: number;
total: number;
results: Customer[];
}
```
To fetch the next page, call `searchCustomer` again with `offset` set to the previous `offset` plus the previous `limit`. Keep requesting pages until `offset + count` reaches `total`.
### Attach result statuses
`searchByEmailAndAttach` never rejects. It reports every outcome through the `status` field of its result, so branch on `status` instead of catching an error:
| Status | Meaning |
| --- | --- |
| `attached` | The Customer was found, and is now on the Cart. The result also carries `customer`. |
| `attach-failed` | The Customer was found, but the Cart update failed. |
| `no-cart` | There was no Cart to attach to, and a Cart couldn't be created. |
| `not-found` | The search succeeded but there were no matching Customers. |
| `search-failed` | The lookup itself failed. |
Distinguish `not-found` from the three failure statuses in your module. Only a failure is worth a retry.
`searchCustomer` reports failure the opposite way. It rejects on a transport or backend failure, so wrap it in `try` and `catch`.
## Render a minimal Customer lookup
Expose a root component that defines your routes:
```tsx title="src/App.tsx"
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { CustomerPage } from './components/CustomerPage';
const App = () => (
} />
);
export default App;
```
Then read the context, attach by email, and render the attached Customer:
```tsx title="src/components/CustomerPage.tsx"
import { useCustomerModuleContext } from 'instoreState/context/CustomerModuleContext';
import { useLocalizationContext } from 'instoreState/locale';
import React, { useCallback, useState } from 'react';
export const CustomerPage = () => {
const { t } = useLocalizationContext();
const { customer, searchByEmailAndAttach, removeCustomer } =
useCustomerModuleContext();
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const attach = useCallback(async () => {
const result = await searchByEmailAndAttach(email);
switch (result.status) {
case 'attached':
return setMessage('');
case 'not-found':
return setMessage(t('Customer.lookup.not_found'));
case 'no-cart':
case 'search-failed':
case 'attach-failed':
return setMessage('Something went wrong. Try again.');
}
}, [email, searchByEmailAndAttach, t]);
if (customer) {
return (
{customer.name}{customer.email}
);
}
return (
setEmail(event.target.value)}
/>
{message}
);
};
```
The POS passes no props to a `Customer` replacement module, so read Customer state from the context.
The translation function `t` returns the key itself when the key is missing. A key that the POS doesn't ship therefore renders as literal text, such as `Customer.lookup.example`. Prefer the keys that the POS already ships, and supply your own fallback text for anything else. For the available keys, see [List of core strings](/instore/customization/list-of-core-strings.md).
## Provide the header Customer control
Register the `LookupCustomer` type to replace the Customer control in the POS header. The context is the same, with three differences:
- The control renders in the header on every route, rather than under a route of its own. Don't define routes in it, and keep the surface compact.
- The POS passes one prop, `enableLookupButton`. It's `true` on the Cart route, and `false` elsewhere.
- A dialog that the control opens renders inside the header. Position the dialog so that the surrounding layout doesn't clip it.
To provide several surfaces from one replacement module, expose a module path for each surface, then register an external module for each type. Those records use the same `scope` and `url`, and differ in `key`, `module`, and `type`.
Render the control from the same context, branching on whether a Customer is attached and on `enableLookupButton`:
```tsx title="src/components/CustomerControl.tsx (pseudocode)"
import { useCustomerModuleContext } from 'instoreState/context/CustomerModuleContext';
import { useLocalizationContext } from 'instoreState/locale';
import React, { useState } from 'react';
type CustomerControlProps = {
enableLookupButton: boolean;
};
export const CustomerControl = ({
enableLookupButton,
}: CustomerControlProps) => {
const { t } = useLocalizationContext();
const { customer, removeCustomer } = useCustomerModuleContext();
const [isDialogOpen, setDialogOpen] = useState(false);
// Pseudocode: implement CustomerLookupDialog using the attach logic from
// "Render a minimal Customer lookup".
if (customer) {
return (
{customer.name}
);
}
if (!enableLookupButton) {
return null;
}
return (
{isDialogOpen && (
setDialogOpen(false)} />
)}
);
};
```
The attached Customer chip renders regardless of `enableLookupButton`; only the lookup button itself respects the prop.
## Customer behavior to account for
A Customer arrives normalized, reshaped by the InStore backend into a flat form, so your module never handles a raw commercetools payload. Localized values like names become plain strings rather than localized objects, and addresses become flat objects. The normalized `customer`, typed as `CustomerModuleCustomer`, has the following shape:
| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | ID of the commercetools Customer. |
| `name` | `string` | Display name, computed from `firstName` and `lastName`. Falls back to a placeholder when both are missing. |
| `email` | `string` | Email address of the Customer. |
| `key`, `customerNumber` | `string`, optional | Pass through from the commercetools Customer. Each falls back to `id`. |
| `firstName`, `lastName`, `title`, `phone`, `companyName`, `vatId` | `string`, optional | Pass through from the commercetools Customer. |
| `dateOfBirth` | `string \ | null`, optional | Date of birth as an ISO date. |
| `addresses` | `object[]`, optional | Flat address objects, with the fields `id`, `key`, `title`, `firstName`, `lastName`, `streetName`, `streetNumber`, `city`, `state`, `postalCode`, `country`, `phone`, and `email`. |
| `defaultShippingAddressId`, `defaultBillingAddressId` | `string \ | null`, optional | IDs into `addresses`. |
| `isEmailVerified` | `boolean`, optional | Whether the email address of the Customer is verified. |
| `customerGroup` | `{ id: string; typeId: string } \ | null`, optional | Reference to the Customer Group the Customer belongs to, or `null` when none is set. |
| `custom` | `Record`, optional | Custom Fields of the Customer, unwrapped from the commercetools `custom` container. |
| `createdAt`, `lastModifiedAt` | `string`, optional | Creation and last-modified timestamps as ISO date-times. |
| `version` | `number`, optional | Current version of the commercetools Customer. |
| `attachStrategy` | `{ type: 'CartAction' \ | 'External'; action: CartUpdateAction[] }` | How the Customer was attached to the Cart. `removeCustomer` replays `action` in reverse. |
Only `id`, `name`, `email`, and `attachStrategy` are required. The InStore backend populates the optional fields too, defaulting them where the commercetools Customer has no value, but a Customer set through the legacy in-POS lookup can lack any of them, including `attachStrategy`. Treat fields beyond `id`, `email`, and `name` as best-effort until that path is retired.
Account for the following behavior:
- The Cart is the authority for `customer`, and the provider keeps the two in sync. A resumed or swapped Cart that already carries a Customer populates or replaces `customer`, and a Cart with no Customer clears it. Render from the context value instead of from the result of your last lookup.
- A return Cart does not clear a Customer while it carries one. The return flow attaches a Customer to the return Cart and leaves the sale Cart alone.
- The provider applies the attach strategy of the Customer on your behalf and records the Cart update actions it applied.
- `removeCustomer` mirrors the attach. It replays the recorded actions with their values removed, which is how commercetools unsets those fields. A Customer attached by email therefore has that email cleared. A Business Unit set during the attach stays on the Cart because that action has no valueless form.
- `removeCustomer` also clears the wishlists and the cached transaction history of the Customer, and it clears the shared state even when the Cart update fails.
- `searchCustomer` resolves an empty page without calling the backend when the workstation is not configured yet. An empty page is therefore not proof that no Customer matches.
- For the query syntax that `searchCustomer` accepts, see [Customer Search](/api/projects/customer-search.md).
`useCustomerModuleContext` does not throw when the Customer context is out of scope. It returns `customer` as `null`, resolves `searchByEmailAndAttach` to `not-found`, and resolves `searchCustomer` to an empty page. A wiring issue appears as a lookup that never finds a Customer instead of as a thrown error. Check this first when an attach never succeeds.
For the provider and import setup, see [Read shared POS state](/instore/customization/pos-module-development.md#read-shared-pos-state).
The Customer module context covers lookup, attach, and detach. The built-in module also displays wishlists, Orders, shipping options, and pay-on-account details, which the context does not expose. A replacement module implements those surfaces itself. For the full set of built-in capabilities, see [InStore\_Customer](/instore/implement-instore/modules/instore-customer.md).
## Next steps
Use the following resources to finish and roll out your Customer module:
- [Build a replacement module](/instore/customization/pos-module-development.md) to configure Module Federation, resolve types, and read shared POS state.
- [Replace InStore POS UI modules](/instore/customization/pos-ui-modules.md) to register the module and assign it to a location or a workstation.
- [Implement the Cart module](/instore/customization/pos-cart-module.md) to read Cart state and render a replacement Cart.
- [Run InStore POS API requests](/instore/customization/pos-api-access.md) to set the API host, Project, and tenant, and to get an access token.
## Related pages
- [Area overview page with navigation](/instore.md)
- [Previous page: Implement the Cart module](/instore/customization/pos-cart-module.md)
- [Search documentation and API specs](/search.md)