---
source_hash: "d58b1dbf"
title: "Advanced Components (High-Level)"
weight: 90
---

# Advanced Components (High-Level)

SmartCommon exposes several "business" components that encapsulate a complete flow (login, device identification, QR/barcode scanning, product catalog navigation, photo annotation, modal "About"). Rather than reinventing these screens in each application, they are injected directly.

All follow the same convention:
- No internal `useTranslation()` -> labels are passed via a `labels` prop
- Style slots (`*Props`) spread on target elements with `twMerge`
- `onSuccess` / `onError` / `onClose` instead of internal side effects
- Compose primitive components `<Modal>`, `<Button>`, `<Input>`, etc.

See also [SmartCommon](/front/smartcommon) for the complete list of components.

## LoginComponent

Complete Dolibarr login form (email + password + optional entity selection + "Remember me" checkbox) with **built-in smartAuth QR pair flow** by default.

### Basic Usage

```javascript
import { LoginComponent } from '@cap-rel/smartcommon';

<LoginComponent
  onSuccess={(user) => navigate("/")}
  onError={(err) => log.error(err)}
/>
```

### With Labels and Options

```javascript
<LoginComponent
  onSuccess={handleSuccess}
  onError={handleError}
  showRememberMe                          // default: false
  showEntities                            // default: true
  enableQrPair                            // default: true
  deviceLabel={navigator.userAgent}
  qrPollIntervalMs={2000}
  qrTimeoutMs={120000}
  labels={{
    emailLabel: t("login.email"),
    passwordLabel: t("login.password"),
    submitLabel: t("login.submit"),
    scanQrLabel: t("login.scan-qr"),
    qrSeparator: t("login.or"),
  }}
  // Custom error label mapping (optional)
  getErrorLabel={(err) => err?.statusCode === 401 ? "Invalid credentials" : null}
/>
```

### Style Slots

`containerProps`, `formProps`, `inputProps`, `passwordInputProps`, `selectProps`, `booleanProps`, `submitButtonProps`, `scanQrButtonProps`, `qrSeparatorProps`, `qrOverlayProps`, `errorAlertProps`, `qrErrorAlertProps`.

Each slot is spread on the target element with `twMerge`, so Tailwind classes resolve correctly (`gap-4` vs `gap-6`).

### Key Behaviors
- HTML5 `required` on email and password (empty field UX handled by browser)
- QR Scan -> claim -> loop polling with global timeout and "Cancel" button
- Idempotency guard: duplicate scan is ignored (avoids 409 on Android with autofocus)
- Full-screen overlay with spinner during claim/poll, camera closed immediately after scan

> [!TIP]
> QR pair is enabled by default because SmartCommon assumes the backend is SmartAuth (which ships `/qr-pair`). To disable: `enableQrPair={false}`.

## DeviceIdentificationComponent

Device identification form for SmartAuth. Reads `useApi().user.deviceOptions` to decide rendering:
- Empty / absent -> "Device name" input only (first device)
- Present -> "Choose an existing device" radio + "New device" option that reveals the input

### Usage

```javascript
import { DeviceIdentificationComponent } from '@cap-rel/smartcommon';

<DeviceIdentificationComponent
  onSuccess={() => navigate("/")}
  onError={(err) => toast.error(err.message)}
  labels={{
    title: t("device.title"),
    devicesDescription: t("device.choose-existing"),
    noDevicesDescription: t("device.first-device"),
    devicesCheckerLabel: t("device.devices"),
    noDeviceLabel: t("device.new-device"),
    newDeviceInputLabel: t("device.label"),
    newDeviceInputHelp: t("device.help"),
    submitLabel: t("device.submit"),
  }}
/>
```

### Style Slots

`containerProps`, `formProps`, `iconWrapperProps`, `iconProps`, `titleProps`, `descriptionProps`, `devicesCheckerProps`, `labelInputProps`, `submitButtonProps`, `errorAlertProps`.

### Notes
- `icon` (default `MdDevices`) can be replaced or disabled (`icon={null}`)
- On submit, `api.identifyDevice({ label, uuid })` is called. The smartAuth endpoint cleans `user.deviceOptions` on the server side, **no need** to manually dispatch.
- `noDeviceValue` (default `"noDevice"`) : value of the "New device" option. Change if it collides with an existing UUID.

## RouteGuard

Guard component for `react-router` that replaces 4 historical custom layouts (`PublicPagesLayout`, `PrivatePagesLayout`, `PreDeviceIdentificationLayout`, `PostDeviceIdentificationLayout`).

Reads `useApi().user` (and `user.deviceOptions` for device modes) and either renders its children (or `<Outlet />` if used as route element), or redirects via `<Navigate>`.

### Four Mutually Orthogonal Modes

```javascript
import { RouteGuard } from '@cap-rel/smartcommon';
import { Routes, Route } from 'react-router-dom';

<Routes>
  // Public pages without user (login, register, forgot-password)
  <Route element={<RouteGuard requireGuest />}>
    <Route path="/login" element={<LoginPage />} />
    <Route path="/welcome" element={<WelcomePage />} />
  </Route>

  // Device identification page: authenticated user + deviceOptions present
  <Route element={<RouteGuard requireDeviceIdentification />}>
    <Route path="/device-identification" element={<DeviceIdentificationPage />} />
  </Route>

  // All private pages: authenticated user + deviceOptions absent
  <Route element={<RouteGuard requireDeviceIdentified />}>
    <Route path="/" element={<HomePage />} />
    <Route path="/settings" element={<SettingsPage />} />
  </Route>

  // Less common: authenticated, regardless of device
  <Route element={<RouteGuard requireAuth />}>
    <Route path="/profile" element={<ProfilePage />} />
  </Route>
</Routes>
```

### Default Redirections

| Mode | Redirects to |
| --- | --- |
| `requireAuth` | `/login` |
| `requireGuest` | `/` |
| `requireDeviceIdentification` | `/` (already identified) |
| `requireDeviceIdentified` | `/device-identification` (to be identified) |

Can be overridden via `redirectTo`. Device modes always imply `requireAuth`: if no user, redirects to `/login` regardless of `redirectTo`.

### Conflicts

`requireAuth` + `requireGuest` or both device modes detected via `console.warn`. The first listed mode wins.

## AboutModal

"About" modal displaying application name, version and free fields. Integrated "Check for updates" button that uses [usePWAUpdate](/front/hooks#usepwaupdate) to restart the Service Worker.

### Usage

```javascript
import { AboutModal } from '@cap-rel/smartcommon';
import { APP_VERSION } from 'src/utils';

<AboutModal
  open={isOpen}
  onClose={() => setIsOpen(false)}
  appName="SmartPOS"
  version={APP_VERSION}
  fields={[
    { label: "Backend", value: prefixUrl },
    { label: "User", value: user?.email },
  ]}
  labels={{
    title: t("about.title"),
    checkUpdates: t("about.check-updates"),
    upToDate: t("about.up-to-date"),
    installUpdate: t("about.install"),
    close: t("about.close"),
  }}
/>
```

### Notes
- `fields` is optional: array of `{ label, value }` displayed as rows
- Labels are in French by default with accents ("À propos", "Vérifier les mises à jour", etc.)
- For custom UI, use [usePWAUpdate](/front/hooks#usepwaupdate) directly

## BarcodeScanner

Full-screen QR/barcode scanner. **Lazy-loads** the `html5-qrcode` dependency (~150 kB) on first open, so apps that never scan don't pay the bundle cost. Falls back to manual entry if camera permission is denied.

### Usage

```javascript
import { BarcodeScanner } from '@cap-rel/smartcommon';

const [open, setOpen] = useState(false);

<BarcodeScanner
  open={open}
  onScan={(text) => addToCart(text)}
  onClose={() => setOpen(false)}
  continuous={false}                  // closes after each scan (default)
  formats={["QR_CODE", "EAN_13"]}     // 7 common formats by default
  debounceMs={1500}
  labels={{
    title: "Scanner",
    cancelButton: "Cancel",
    manualEntry: "Manual entry",
  }}
/>
```

### Notes
- `continuous={true}` keeps the scanner open after each scan (useful for batch entry)
- `formats`: `html5-qrcode` names (`QR_CODE`, `EAN_13`, `CODE_128`, etc.)
- The component manages camera closing, unmount, permissions itself

## ProductCategoryBrowser

Full-screen modal that allows the user to navigate through a product catalog by category and select one (or multiple). Used anywhere an app needs to attach products to an entity: invoice/quote lines, photo annotations (`smartintervention`), inventory, repair quotes, etc.

### Three Modes

| Mode | Confirmation Step | `onSelect` Payload (single) |
| --- | --- | --- |
| `select` | no | `product` |
| `quantity` | quantity input | `{ product, qty }` |
| `quantity-discount` | qty + discount + total | `{ product, qty, discountPercent, computedTotal }` |

With `multiple={true}`, a cart at the page footer is maintained and `onSelect` is called once on validation with an array of the same format.

### Adapters (No Dexie Coupling)

Data is fetched via two adapters that the app provides:

```javascript
// Expected shape
productsAdapter = {
  search:  ({ categoryId, query, type }) => Promise<Product[]>,
  getById: (id) => Promise<Product>,
};

categoriesAdapter = {
  getRoots:    (type)     => Promise<Category[]>,
  getChildren: (parentId) => Promise<Category[]>,
  getById:     (id)       => Promise<Category>,
};
```

Conventions:
- `categoryId === undefined` -> all products
- `categoryId === null` -> products without category
- `categoryId === <number>` -> products in that category
- `query`: free text (300ms debounce managed by component)
- `type`: passthrough filter (e.g., Dolibarr 0=product / 1=service)

### Dexie Helper for offlinepropale-style Apps

```javascript
import { ProductCategoryBrowser, createDexieProductCategoryAdapters } from '@cap-rel/smartcommon';
import db from 'src/db';

const { productsAdapter, categoriesAdapter } = createDexieProductCategoryAdapters({ db });

<ProductCategoryBrowser
  open={open}
  onClose={() => setOpen(false)}
  mode="quantity-discount"
  productsAdapter={productsAdapter}
  categoriesAdapter={categoriesAdapter}
  productType={0}                                 // 0=product, 1=service
  customerContext={{ priceLevel: customer.priceLevel }}
  getProductPriceDisplay={(product, ctx) => ({
    unitPrice: product.price,
    displayPriceLabel: `${product.price} ${ctx.currency}`,
    badge: product.discount ? `-${product.discount}%` : null,
  })}
  onSelect={(payload) => addLine(payload)}
/>
```

The helper is calibrated on the offlinepropale Dexie schema:
- Tables `products`, `categories`, `productDocuments`, `categoryDocuments`
- Many-to-many via denormalized `product.categories[]`
- Filters `for_sale`/`tosell`/`status_sell` to exclude inactive
- Category type alias: `"product"` <-> `[0, "0", "product"]`
- Bulk image joins from `productDocuments`/`categoryDocuments` filtered `type === "image"`

All table/field names are overrideable via options. For very large catalogs (>5000 products) pass `attachImages: false` and provide a custom `renderItem` that lazy loads thumbnails.

### Editing an Existing Line

`prefillProduct` (with `defaultQty` / `defaultDiscountPercent`) opens directly on the confirmation step:

```javascript
<ProductCategoryBrowser
  open={open}
  mode="quantity-discount"
  prefillProduct={annotation.product}
  defaultQty={annotation.qty}
  defaultDiscountPercent={annotation.remise_percent}
  onSelect={(payload) => updateAnnotation(annotation.id, payload)}
  onClose={...}
/>
```

The user can tap "Change product" to return to the grid; the qty/discount fields are then reset to the default values of the new product.

### Price Hook

`getProductPriceDisplay(product, customerContext)` returns:

```javascript
{
  unitPrice,           // number
  displayPrice?,       // number
  displayPriceLabel?,  // string: rendered verbatim if provided
  currency,
  badge?,              // discount badge (e.g., "-20%")
  ttc?,                // boolean
}
```

`customerContext` is free: customer id, price level, currency, etc. The component just passes it to the hook.

## PhotoAnnotator

Allows users to place markers on a photo, each marker linked to a business object that the app defines (note, product, alert, sub-photo). Used by smartintervention (technician marking defective parts), repair quotes, building inspection, offlinepropale photo quotes, etc.

### Two Modes

**Controlled** (in-memory state, simple):

```javascript
import { PhotoAnnotator } from '@cap-rel/smartcommon';

const [annotations, setAnnotations] = useState([]);

<PhotoAnnotator
  src={photo.url}
  annotations={annotations}
  onChange={setAnnotations}
  annotationTypes={...}
/>
```

**Event-based** (backend persistence, granular callbacks):

```javascript
<PhotoAnnotator
  src={photo.url}
  initialAnnotations={anns}             // loaded once; pass a new ref to resync
  annotationTypes={...}
  onCreate={async (staged) => {
    const id = await db.annotations.add({...});
    return { ...staged, id };           // component adopts the new id
  }}
  onUpdate={async (annotation) => { await db.update(annotation.id, ...); }}
  onMove={async (annotation, { x, y }) => { await db.update(annotation.id, { pos_x: x, pos_y: y }); }}
  onDelete={async (annotation) => { await db.delete(annotation.id); }}
/>
```

Event-based mode is automatically detected when any of `onCreate` / `onUpdate` / `onMove` / `onDelete` / `initialAnnotations` is provided.

### Annotation Shape

```javascript
{
  id: string|number,    // stable identifier
  type: string,         // key in annotationTypes
  x: number,            // 0..100 (percentage)
  y: number,            // 0..100 (percentage)
  payload?: object,     // free, owned by the type
}
```

### Type Registry

Each entry in `annotationTypes` is a `TypeDef`:

```javascript
{
  label: string,
  icon: ReactNode,
  color?: string,
  newPayload?: () => object,
  renderMarker:    (annotation, ctx) => ReactNode, // ctx = { num, selected, dragging, readOnly }
  renderEditor:    (annotation, ctx) => ReactNode, // ctx = { onSave(partial), onCancel, typeDef }
  renderListItem?: (annotation, ctx) => ReactNode,
  headlessEditor?: boolean,                        // no modal, see below
}
```

### Headless Editor

For types that do not require modal UI (e.g., trigger a file input then save), set `headlessEditor: true`. The component then mounts the return value of `renderEditor` directly, without modal wrapping:

```javascript
photo: {
  label: "Detailed Photo",
  icon: <FaCamera />,
  headlessEditor: true,
  renderMarker: (a, { num }) => <CameraCircle num={num} />,
  renderEditor: (a, { onSave, onCancel }) => (
    <PhotoCaptureFlow
      onCaptured={async (blob) => {
        const targetPhotoId = await imagesService.create(blob);
        onSave({ payload: { targetPhotoId } });
      }}
      onCancel={onCancel}
    />
  ),
}
```

### Composition with ProductCategoryBrowser

A "product" type delegates its editor to the catalog:

```javascript
const productType = {
  label: "Product",
  icon: <FaBoxesStacked />,
  color: "#3B82F6",
  renderMarker: (a, { num }) => <Circle color="#3B82F6">{num}</Circle>,
  renderEditor: (a, { onSave, onCancel }) => (
    <ProductCategoryBrowser
      open
      mode="quantity-discount"
      productsAdapter={productsAdapter}
      categoriesAdapter={categoriesAdapter}
      prefillProduct={a.payload?.fk_product ? { id: a.payload.fk_product } : undefined}
      defaultQty={a.payload?.qty || 1}
      defaultDiscountPercent={a.payload?.remise_percent || 0}
      onSelect={({ product, qty, discountPercent, computedTotal }) =>
        onSave({ payload: {
          fk_product: product.id,
          qty,
          remise_percent: discountPercent,
          computed_total: computedTotal,
        } })
      }
      onClose={onCancel}
    />
  ),
};
```

### Interactions
- Long press background -> `TypePicker` (or direct editor if only 1 type)
- "+ Add" button -> creates at center (50%, 50%), persists, opens editor
- Tap marker -> selection (`onAnnotationSelect`)
- Double tap marker -> `onAnnotationActivate` (drill-in)
- Long press marker -> drag; `onChange` triggered once on `pointerup`
- Pinch / wheel -> zoom (clamped `[minZoom, maxZoom]`)
- Drag one finger on zoomed background -> pan
- Edit/delete buttons in list (`window.confirm` for deletion)
- `readOnly` disables add/edit/delete/drag; tap and double tap remain active

### Image Source

`src` accepts:
- URL string (passthrough)
- Blob / File (auto `createObjectURL`/`revokeObjectURL` via `useImageUrl`)

### Layout

`listPosition`:
- `"bottom"` (default) -> list below image
- `"right"` -> sidebar (desktop)
- `"off"` -> no list, the app renders its own

## Exported Helpers

Some utilities are exported alongside components:

| Helper | Description |
| --- | --- |
| `createDexieProductCategoryAdapters` | Builds both `productsAdapter` / `categoriesAdapter` from a Dexie instance compatible with the offlinepropale schema |
| `extractPairingId` | Parses a scanned QR payload (raw string, URL `/qrpair/...`, JSON `pairing_id`) and returns the 32-hex pairing_id or `null` |
| `buildDefaultGetQrErrorLabel(labels)` | Builds the default `getQrErrorLabel` function used in `<LoginComponent>`. Re-exported to be reused if the app partially overrides the mapping |
| `twMerge` | Re-export of `tailwind-merge`. To be used to merge default `className` + consumer `className` (otherwise `gap-4`/`gap-6` conflicts are not resolved correctly) |

## See Also
- [SmartCommon](/front/smartcommon) - Complete Component List
- [Hooks](/front/hooks) - useApi (claimQrPair / pollQrPair / identifyDevice), usePWAUpdate
- [Routing](/front/routage) - Detailed RouteGuard Usage
- [SmartAuth](/smartauth) - QR Pair Backend and Device Identification
- [PWA](/front/pwa) - Service Worker Update Strategy
