# ls-signer

## Overview

**ls-signer** is a Stencil-based web component that wraps a React + TypeScript application for electronic document signing workflows. It is designed to be embedded in any host application (e.g. the Lobby project) as a custom element (`<ls-signer>`).

The project is composed of three layers:

- **Stencil wrapper** (`src/`) — Defines the `<ls-signer>` custom element, which dynamically mounts the React app.
- **React app** (`react-app/`) — UI layer built with React 19, Vite, SCSS (BEM), i18next, and React Router.
- **Signer actions** (`signer-actions/`) — Business logic layer containing hooks, stores (Zustand), services, models, and utilities.

---

## Setup .env

Copy `.env.example` to `.env` and fill in the values:

```
# Amazon Translate (for pnpm translate)
AWS_TRANSLATE_ACCESS_KEY_ID=<translate_user_access_key>
AWS_TRANSLATE_SECRET_ACCESS_KEY=<translate_user_secret_key>
```

---

## Architecture

```
<ls-signer>  (Stencil web component)
  └── React App  (UI — pages, components, routing)
        └── Signer Actions  (business logic — hooks, stores, services)
```

The Stencil component loads the React bundle at runtime and mounts it into a `<div>` inside the custom element's DOM. The React app handles all UI and routing, while signer-actions manages state and API interactions.

---

## Project Structure

```
ls-signer/
├── src/                        # Stencil web component
│   └── components/
│       └── ls-signer/
│           └── ls-signer.tsx   # Custom element definition
├── react-app/                  # React UI layer
│   ├── App.tsx
│   ├── index.ts                # Library entry point
│   ├── react-entry.tsx         # React component export
│   ├── i18n.ts
│   ├── style.scss
│   ├── components/             # Reusable UI components
│   ├── pages/                  # Page-level components
│   ├── config/                 # App configuration
│   ├── contexts/               # React contexts
│   ├── styles/                 # SCSS partials and global styles
│   ├── types/                  # TypeScript type definitions
│   ├── utils/                  # UI utilities
│   └── mocks/                  # Mock data for development
├── signer-actions/             # Business logic layer
│   ├── hooks/                  # Custom hooks
│   ├── stores/                 # Zustand state stores
│   ├── services/               # API services
│   ├── models/                 # Data models
│   ├── types/                  # Shared type definitions
│   ├── utils/                  # Business logic utilities
│   └── mocks/                  # Mock data for testing
├── dist-react/                 # Built React bundle (react-app.js, react-app.css)
├── loader/                     # Stencil lazy-loading entry points
├── www/                        # Stencil dev server output
├── stencil.config.ts           # Stencil configuration
├── vite.config.ts              # Vite dev server config
├── vite.lib.config.ts          # Vite library build config (React → ES module)
├── tsconfig.json
├── tsconfig.stencil.json
└── package.json
```

---

## Build Pipeline

The build process has two stages:

1. **React bundle** — Vite builds `react-app/` into a single ES module (`dist-react/react-app.js` + CSS).
2. **Stencil component** — Stencil compiles `src/` into a distributable web component, then copies the React bundle into `dist/ls-signer/`.

```sh
# Full build (both stages)
pnpm build

# React only
pnpm build:react

# Stencil only
pnpm build:stencil
```

---

## Features

- **Web Component**: Framework-agnostic `<ls-signer>` custom element via Stencil.
- **React 19 + TypeScript**: Functional components with logic extracted into custom hooks.
- **Vite**: Fast development and library builds.
- **REST & GraphQL**: Data fetching via REST and AWS Amplify GraphQL.
- **State Management**: Zustand stores in signer-actions.
- **Internationalization**: i18next with 18 languages, auto-sync via Amazon Translate.
- **Routing**: React Router for internal navigation.
- **Accessibility**: ARIA labels, keyboard navigation, color contrast, and focus management.
- **SCSS**: BEM conventions for styling.

---

## Components

- **Auth**: Authentication workflows (AWS Cognito, username/password, Google OAuth)
- **Documents**: Document list/dashboard with status categorization
- **Signing**: Main document signing interface with field management
- **Modals**: Signature capture, terms acceptance, rejection, and dialogs

---

## Route Types

- `/` — Authentication page (redirects to `/documents` if authenticated)
- `/documents` — Document dashboard (requires authentication)
- `/robot-check` — Robot verification page (public)
- `/{uuid}` — Active document signing page
- `/{uuid}/completed` — Document completion page
- `/{uuid}/expired` — Document expiration notice
- `/{uuid}/removed` — Document removed notice
- `/{uuid}/noaccount` — Account closed notice
- `/{uuid}/notwitnessed` — Missing witness notice
- `/{uuid}/link-sent` — Link sent confirmation
- `/{uuid}/completed-bundle` — Bundle completion page
- `/{uuid}/unknown` — Unknown error page

---

## API Endpoints

All API requests go through a shared utility with authentication header injection.

- `GET /signing/{uuid}/` — Load document details
- `GET /signing/documents/` — List user documents
- `POST /signing/{uuid}/save-field/` — Save field data
- `POST /signing/{uuid}/sign/` — Submit signed document
- `GET /signing/{uuid}/status/` — Check signing status
- `GET /signing/{uuid}/download/` — Download completed PDF

---

## Coding Standards

- Functional components only, no class components
- Logic extracted into custom hooks (e.g. `useDocumentCard`)
- Arrow functions preferred
- Strict TypeScript — avoid `any`, use type guards
- PascalCase for components, camelCase for utilities/folders, UPPER_CASE for constants
- ES Module imports only
- SCSS with BEM naming conventions

---

## Getting Started

### Prerequisites

- Node.js (v18+)
- pnpm
- [ls-signer-testing](https://github.com/legalesign/ls-signer-testing) cloned as a sibling directory

### Installation

```sh
cd ls-signer
pnpm install
```

### Development

This project is a component library — it does not have its own dev server. Use [ls-signer-testing](https://github.com/legalesign/ls-signer-testing) for local development:

```sh
# In the sibling ls-signer-testing directory
cd ../ls-signer-testing
pnpm start
```

This starts a Vite dev server with HMR that aliases imports to your local ls-signer source. Any changes in `react-app/` or `signer-actions/` hot-reload instantly.

### Build

```sh
pnpm build
```

### Testing

Cypress E2E tests run against ls-signer-testing:

```sh
# Terminal 1: start the dev server
cd ../ls-signer-testing
pnpm start

# Terminal 2: run Cypress
cd ../ls-signer
pnpm cy:open
```

In CI, the Cypress workflow automatically clones ls-signer-testing and starts it with the PR's code.

---

## Usage

The package provides two ways to use the signing component:

| Option | Best for | Import |
|---|---|---|
| Web Component (Stencil) | Any framework or vanilla HTML | CDN script tag |
| React Component | React applications | `legalesign-signer/react` |

### Option 1: Web Component (any framework)

The `<ls-signer>` custom element works in any environment — vanilla HTML, Vue, Angular, Svelte, or any other framework.

Add the script tag to your HTML:

```html
<script type="module" src="https://cdn.legalesign.io/signer/latest/ls-signer.esm.js"></script>
```

Then use it anywhere in your templates:

```html
<ls-signer
  recipient-id="abc123"
  private-key="key"
  session-id="session"
  api-base-url="https://app1.legalesign.com"
></ls-signer>

<script>
  const signer = document.querySelector('ls-signer');
  signer.addEventListener('signingSuccess', (e) => console.log('Signed:', e.detail));
  signer.addEventListener('signingFail', (e) => console.error('Failed:', e.detail));
</script>
```

#### Props (attributes)

| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
| `recipient-id` | `string` | ✅ | — | Recipient identifier (base64, `rec` prefix, or UUID) |
| `private-key` | `string` | ✅ | — | Private key from session API |
| `session-id` | `string` | ✅ | — | Session identifier |
| `api-base-url` | `string` | ❌ | `https://app1.legalesign.com` | API base URL |
| `colour` | `string` | ❌ | — | Theme colour (see [Custom Theming](#custom-theming)) |

#### Events

| Event | Detail | Description |
|---|---|---|
| `signingSuccess` | `{ documentId, recipientId }` | Emitted when signing completes |
| `signingFail` | `{ error, documentId?, recipientId }` | Emitted on signing failure |

#### Vue example

Add the script tag to your `index.html`:
```html
<script type="module" src="https://cdn.legalesign.io/signer/latest/ls-signer.esm.js"></script>
```

Then use in your component:
```vue
<template>
  <ls-signer
    :recipient-id="recipientId"
    :private-key="privateKey"
    :session-id="sessionId"
    @signingSuccess="handleSuccess"
    @signingFail="handleFail"
  />
</template>
```

> **Note:** If using Vue with Vite, add `isCustomElement: (tag) => tag.startsWith('ls-')` to your Vue plugin config to suppress unknown element warnings.

---

### Option 2: React Component

For React applications, install the package and import the component directly — no web component overhead, native props/events, and full TypeScript support.

```sh
pnpm add legalesign-signer
```

```tsx
import { LsSigner } from 'legalesign-signer/react';
import 'legalesign-signer/react.css';

const SigningPage = () => (
  <LsSigner
    recipientId="abc123"
    privateKey="key"
    sessionId="session"
    apiBaseUrl="https://app1.legalesign.com"
    onSuccess={({ documentId, recipientId }) => console.log('Signed:', documentId)}
    onFail={({ error }) => console.error('Failed:', error)}
  />
);
```

#### Props

| Prop | Type | Required | Description |
|---|---|---|---|
| `recipientId` | `string` | ✅ | Recipient identifier (base64, `rec` prefix, or UUID) |
| `privateKey` | `string` | ✅ | Private key from session API |
| `sessionId` | `string` | ✅ | Session identifier |
| `apiBaseUrl` | `string` | ❌ | API base URL |
| `colour` | `LsSignerColour` | ❌ | Theme colour (see [Custom Theming](#custom-theming)) |
| `onSuccess` | `(data: { documentId: string; recipientId: string }) => void` | ❌ | Called on successful signing |
| `onFail` | `(data: { error: string; documentId?: string; recipientId: string }) => void` | ❌ | Called on signing failure |

#### Peer dependencies

The React entry requires `react` and `react-dom` (v18 or v19) as peer dependencies — these are not bundled.

---

## Custom Theming

The component supports colour theming via a single `colour` prop. Pick a colour name and all shades are set automatically from the legalesign-ui palette.

### Available Colours

`pink`, `blue`, `purple`, `indigo`, `teal`, `green`, `lightblue`, `burnt`, `aubergine`, `red`, `yellow`, `cyan`, `lime`, `trueGreen`

Omit the prop for the default blue.

### Web Component (Stencil)

```html
<ls-signer
  colour="pink"
  recipient-id="abc123"
  private-key="key"
  session-id="session"
></ls-signer>
```

### React Component

```tsx
import { LsSigner } from 'legalesign-signer/react';
import 'legalesign-signer/react.css';

const SigningPage = () => (
  <LsSigner
    colour="pink"
    recipientId="abc123"
    privateKey="key"
    sessionId="session"
  />
);
```

TypeScript provides autocomplete for all available colour values via the `LsSignerColour` type.

### How It Works

The `colour` prop sets a `data-ls-theme` attribute on the component's root element. The CSS defines a full 10–100 shade scale (sourced from `legalesign-ui`) for each colour, which overrides the default palette via CSS custom property inheritance.

| Shade | Usage |
|---|---|
| 10 | Light backgrounds, subtle fills |
| 20 | Subtle borders |
| 30 | Focus rings |
| 60 | Primary colour (buttons, links, active states) |
| 70 | Hover state |
| 80 | Dark/strong accents |

---

## Internationalization (i18n)

The app uses [i18next](https://www.i18next.com/) with translations bundled directly into the build. All user-facing strings are translated — no hardcoded English in components.

### Supported Languages

| Code | Language | Code | Language |
|------|----------|------|----------|
| `en` | English | `nl` | Dutch |
| `fr` | French | `fi` | Finnish |
| `bg` | Bulgarian | `it` | Italian |
| `es` | Spanish | `he` | Hebrew |
| `de` | German | `sv` | Swedish |
| `gs` | Scottish Gaelic | `cy` | Welsh |
| `ar` | Arabic | `is` | Icelandic |
| `el` | Greek | `iw` | Hebrew (legacy) |
| `pt` | Portuguese | | |
| `ro` | Romanian | | |

### File Structure

```
react-app/locales/
├── en/translation.json    ← source of truth
├── fr/translation.json
├── bg/translation.json
└── .../translation.json
```

Translations are imported in `react-app/i18n.ts` and bundled at build time. The language selector in the sidebar reads available languages dynamically from i18n config.

Each translation file contains a `common.languagename` key with the language's native name (e.g. `"Français"` for French) used by the language selector.

### Translation Keys

All keys use single lowercase words with no separators:

```json
{
  "common": {
    "cancel": "Cancel",
    "pleasewait": "Please wait...",
    "tryagain": "Try Again"
  },
  "signing": {
    "fieldscompleteapprove": "Fields complete - approve document"
  }
}
```

Interpolation uses i18next `{{variable}}` syntax:

```json
{
  "sidebar": {
    "approvenow": "{{action}} now",
    "expires": "Expires {{date}}"
  }
}
```

### Adding a New String

1. Add the key to `react-app/locales/en/translation.json`
2. Use it in a component:
   ```tsx
   const { t } = useTranslation();
   return <p>{t('common.newkey')}</p>;
   ```
3. Run `pnpm translate` to translate and sync all languages
4. Commit the updated translation files with your PR

### Removing a String

1. Remove the key from `react-app/locales/en/translation.json`
2. Run `pnpm translate` — it removes the key from all other languages automatically
3. Commit the updated translation files with your PR

### Adding a New Language

1. Create `react-app/locales/{code}/translation.json` (can be empty `{}`)
2. Add the import and registration in `react-app/i18n.ts`
3. Add the code to `supportedLngs` in `react-app/i18n.ts`
4. Run `pnpm translate` — it fills all keys automatically

The sidebar language selector picks up new languages automatically from `i18n.options.supportedLngs`.

### Sync Script

The sync script keeps all translation files in sync with English. **Run this manually whenever you add, remove, or change translation keys.**

```sh
# Sync all languages with Amazon Translate
pnpm translate

# Check only — exits with code 1 if out of sync
pnpm translate:check
```

The `translate` command reads AWS credentials from the project `.env` file. Add these two lines:

```
AWS_TRANSLATE_ACCESS_KEY_ID=<translate_user_access_key>
AWS_TRANSLATE_SECRET_ACCESS_KEY=<translate_user_secret_key>
```

Without credentials, missing keys are filled with `[NEEDS TRANSLATION] English text` placeholders.

The script (`scripts/sync-translations.cjs`):
- Reads `en/translation.json` as the source of truth
- Adds missing keys to every other language (auto-translated or placeholder)
- Removes extra keys not present in English
- Reorders keys to match English structure
- Preserves existing translated values

#### AWS IAM Policy for Translation

To use Amazon Translate, you need an IAM user with this policy:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "translate:TranslateText",
      "Resource": "*"
    }
  ]
}
```

---

## Release Versioning

```
major.minor.patch
0.1.0
```

Versioning is automated via GitHub Actions when pushing to the `prod` branch. The version is read from `package.json` and injected into the app at build time via Vite's `define` as `__APP_VERSION__`. It is displayed in the sidebar bottom bar, to the right of the language selector.

### How It Works

1. PR is merged to `prod`
2. GitHub Actions workflow (`.github/workflows/publish.yml`) runs
3. Version in `package.json` is bumped based on PR title
4. Package is built and published to npm
5. The bump is committed back to `prod` with `[skip ci]` and a git tag is pushed

### PR Title Format

| PR title | Bump | Example |
|---|---|---|
| `fix: ...`, `chore: ...`, anything else | patch | 0.1.0 → 0.1.1 |
| `feat: ...`, `feat(...): ...` | minor | 0.1.0 → 0.2.0 |
| Contains `BREAKING CHANGE` | major | 0.1.0 → 1.0.0 |

```bash
# Patch (default)
fix: resolve field positioning issue

# Minor
feat: add bulk download

# Major
feat: new signing architecture BREAKING CHANGE
```

### Secrets

The workflow requires two secrets configured in **GitHub → ls-signer repo → Settings → Secrets and variables → Actions**:

| Secret | What | Scope | Used for |
|---|---|---|---|
| `PAT_TOKEN` | GitHub classic Personal Access Token | `repo` (full control) | Pushing version bump commits and tags back to `prod` |
| `NPM_TOKEN` | npm granular access token | All packages, bypass 2FA | Publishing to npm |

- **Created at:** [GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)](https://github.com/settings/tokens)
- If you already have a `PAT_TOKEN` from reporting or bulksend, you can reuse the same token — just add it as a secret in the ls-signer repo
- GitHub sends email notifications before tokens expire
- If the token expires, the workflow will fail at the `git push` step with an auth error — create a new token and update the secret

### Branch Protection

If `prod` has branch protection enabled (requires PRs), you need a bypass rule:

**GitHub → ls-signer repo → Settings → Branches → `prod` rule → "Allow specified actors to bypass required pull requests"**

---

## References

- [Stencil](https://stenciljs.com/)
- [React](https://react.dev/)
- [TypeScript](https://www.typescriptlang.org/)
- [Vite](https://vitejs.dev/)
- [AWS Amplify](https://docs.amplify.aws/)
- [Zustand](https://zustand.docs.pmnd.rs/)
- [i18next](https://www.i18next.com/)

---