# 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)