# PixaProof Web SDK - Integration Guide

## Table of Contents

1. [Getting Started](#getting-started)
   - [Requirements](#requirements)
   - [Installation](#installation)
2. [Examples](#examples)
   - [Basic](#basic-example)
   - [Advanced](#advanced-example)
3. [API Reference](#api-reference)
   - [PixaProof Backend API](#pixaproof-backend-api)
   - [SDK API](#sdk-api)

---

## Getting Started

### Requirements

| Dependency | Version | Notes |
|-----------|---------|-------|
| [pako](https://github.com/nodeca/pako) | ^2.x | Required for compression. **Must be loaded before the SDK.** |
| HTTPS | - | Camera and motion sensor APIs require a secure context. The SDK will not work over plain HTTP. |

### Installation

1. Download `pixaproof.umd.js` and host it alongside your page.
2. Load **pako** from CDN first, then load the SDK script.

```html
<!-- 1. Load pako BEFORE the SDK -->
<script src="https://cdn.jsdelivr.net/npm/pako@2/dist/pako.min.js"></script>

<!-- 2. Load the PixaProof SDK -->
<script src="https://cdn.jsdelivr.net/npm/pixaproof-web-sdk/dist/pixaproof.umd.js"></script>
```

The SDK is then available globally as:

```js
const sdk = new PixaProof({
  apiUrl:    "{url}",
  apiKey:    "YOUR_API_KEY",
  token:     "YOUR_ACCESS_TOKEN_FROM_BACKEND",
  elementId: "pixaproof-camera",
  preferredCamera: "environment", // optional: "environment" (default), "user", or "any"
});
```

---

## Examples

### Basic Example

A minimal integration - only the required options and the most important callbacks.

> **Token note**: The `token` field expects an OAuth2 Bearer access token. **This token must be obtained from your own backend server** (which calls `/oauth2/token` with your `client_id` and `client_secret`) and then passed down to the frontend. Never call `/oauth2/token` directly from the browser, as doing so would expose your `client_secret`.

```html
<!DOCTYPE html>
<html>
<head>
  <title>PixaProof - Basic Example</title>
</head>
<body>

  <!-- Container element where the SDK renders the camera view -->
  <div id="pixaproof-camera"></div>

  <div id="status">Initializing...</div>
  <div id="error" style="color: red;"></div>

  <button id="captureBtn" style="display: none;">Capture Image</button>

  <!-- 1. pako must come before the SDK -->
  <script src="https://cdn.jsdelivr.net/npm/pako@2/dist/pako.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/pixaproof-web-sdk/dist/pixaproof.umd.js"></script>

  <script>

    // Token should come from your backend after calling /oauth2/token server-side
    const accessToken = "YOUR_ACCESS_TOKEN_FROM_BACKEND";

    const sdk = new PixaProof({
      apiUrl:    "{url}",
      apiKey:    "YOUR_API_KEY",
      token:     accessToken,
      elementId: "pixaproof-camera",

      onInitSuccess() {
        document.getElementById("status").textContent = "Camera ready. Press Capture.";
        document.getElementById("captureBtn").style.display = "inline-block";
      },

      onInitError(error) {
        document.getElementById("error").textContent = "Init failed: " + error.message;
      },

      onCaptureSuccess(blob) {
        console.log("Captured image blob:", blob);
        // Send `blob` to your backend for verification (see POST /api/v1/verification/image)
      },

      onCaptureError(error) {
        document.getElementById("error").textContent = "Capture failed: " + error.message;
      },
    });

    // Render the permission UI and open the camera
    sdk.init();

    document.getElementById("captureBtn").addEventListener("click", () => {
      sdk.captureCamera().then(() => {
        sdk.closeCamera();
      });
    });
  </script>

</body>
</html>
```

---

### Advanced Example

Demonstrates all available options, all callbacks, UI customization, and non-critical permission banners.

```html
<!DOCTYPE html>
<html>
<head>
  <title>PixaProof - Advanced Example</title>
  <style>
    #captureBtn { padding: 10px 24px; font-size: 15px; margin-top: 16px; display: none; }
    #status     { margin: 10px 0; color: #555; }
    #error      { color: #c00; }
  </style>
</head>
<body>

  <div id="pixaproof-camera"></div>

  <div id="status">Initializing...</div>
  <div id="error"></div>
  <button id="captureBtn">Capture Image</button>

  <script src="https://cdn.jsdelivr.net/npm/pako@2/dist/pako.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/pixaproof-web-sdk/dist/pixaproof.umd.js"></script>

  <script>

    // ─── Permission banner helper ──────────────────────────────────────────────
    // Renders a scrolling notification at the bottom of the camera view.
    // Must be called after onInitSuccess so the video wrapper exists in the DOM.

    function showPermissionBanner(elementId, message) {
      const videoWrapper = document.querySelector("#" + elementId + " .video-wrapper");
      if (!videoWrapper) return;

      const existing = videoWrapper.querySelector(".pixaproof-permission-banner");
      if (existing) existing.remove();

      if (!document.getElementById("pixaproof-banner-styles")) {
        const style = document.createElement("style");
        style.id = "pixaproof-banner-styles";
        style.textContent = `
          @keyframes marqueeScroll {
            0%   { transform: translateX(0); }
            100% { transform: translateX(-50%); }
          }
          .pixaproof-permission-banner {
            position: absolute; bottom: 16px; left: 50%;
            transform: translateX(-50%); max-width: 80%;
            background: #333; display: inline-flex;
            align-items: center; justify-content: center;
            padding: 12px 20px; border-radius: 8px;
            z-index: 1000; overflow: hidden; white-space: nowrap;
          }
          .pixaproof-banner-message {
            overflow: hidden; white-space: nowrap; color: #fff;
            font-size: 14px; font-family: system-ui, sans-serif;
          }
          .pixaproof-banner-inner { display: inline-flex; align-items: center; }
          .pixaproof-banner-inner > span { display: inline-block; padding-right: 32px; }
          .pixaproof-banner-scroll .pixaproof-banner-inner {
            animation: marqueeScroll 18s linear infinite;
          }
          @media (max-width: 768px) {
            .pixaproof-permission-banner { padding: 8px 12px; bottom: 12px; max-width: 85%; }
            .pixaproof-banner-message { font-size: 12px; }
          }
        `;
        document.head.appendChild(style);
      }

      const banner  = document.createElement("div");
      banner.className = "pixaproof-permission-banner";

      const msgEl = document.createElement("div");
      msgEl.className = "pixaproof-banner-message";

      const inner = document.createElement("div");
      inner.className = "pixaproof-banner-inner";

      const spanA = document.createElement("span"); spanA.textContent = message;
      const spanB = document.createElement("span"); spanB.textContent = message;

      inner.appendChild(spanA);
      inner.appendChild(spanB);
      msgEl.appendChild(inner);
      banner.appendChild(msgEl);
      videoWrapper.appendChild(banner);

      setTimeout(() => {
        if (spanA.scrollWidth > banner.clientWidth) {
          msgEl.classList.add("pixaproof-banner-scroll");
        } else {
          spanB.style.display = "none";
        }
      }, 0);
    }

    // ─── Track non-critical permission failures ────────────────────────────────
    // These callbacks fire before the camera view exists, so we store the state
    // and render the banners later inside onInitSuccess.

    let locationPermissionFailed = false;
    let sensorPermissionFailed   = false;

    // ─── SDK initialisation ───────────────────────────────────────────────────

    const sdk = new PixaProof({
      apiUrl:    "{url}",
      apiKey:    "YOUR_API_KEY",
      token:     "YOUR_ACCESS_TOKEN_FROM_BACKEND",  // Obtain server-side via /oauth2/token
      elementId: "pixaproof-camera",

      // Optional: custom dimensions (defaults: 640 × 480)
      width:  640,
      height: 480,

      // Optional: prefer the rear camera by default, with graceful fallback if it cannot start
      preferredCamera: "environment",

      // Optional: customise the permission prompt overlay
      permissionUI: {
        overlay: {
          styles: { "background-color": "rgba(0, 0, 0, 0.75)" },
          // className: "my-overlay-class",  // Alternative: use a CSS class
        },
        container: {
          styles: { "background": "linear-gradient(135deg, #667eea, #764ba2)", "border-radius": "12px" },
        },
        message: {
          // Use the html option to render rich content inside the message
          html: 'Please allow <strong>camera access</strong> to continue.',
          styles: { "color": "#fff", "font-size": "16px" },
        },
        button: {
          text:        "Allow Access",
          loadingText: "Requesting…",
          styles:      { "background-color": "#28a745", "color": "#fff" },
        },
      },

      // Optional: customise individual camera UI elements
      cameraUI: {
        wrapper:      { styles: { "border-radius": "12px", "overflow": "hidden" } },
        card:         { styles: { "box-shadow": "0 4px 20px rgba(0,0,0,0.3)" } },
        videoWrapper: { styles: {} },
        video:        { styles: { "object-fit": "cover" } },
        canvas:       { styles: {} },
        snapshot:     { styles: {} },
      },

      // ── Lifecycle callbacks ──────────────────────────────────────────────────

      onInit() {
        console.log("[PixaProof] Initialization started");
        document.getElementById("status").textContent = "Initializing…";
      },

      onInitSuccess(authResponse) {
        console.log("[PixaProof] Ready", authResponse);
        document.getElementById("status").textContent = "Camera ready. Press Capture.";
        document.getElementById("captureBtn").style.display = "inline-block";

        // Now that the camera view exists in the DOM, show any deferred banners
        if (locationPermissionFailed) {
          showPermissionBanner("pixaproof-camera", "Location permission required for full accuracy");
        }
        if (sensorPermissionFailed) {
          showPermissionBanner("pixaproof-camera", "Motion sensor permission required to continue");
        }
      },

      onInitError(error) {
        console.error("[PixaProof] Init error", error);
        document.getElementById("error").textContent = "Init failed: " + error.message;
      },

      // Camera permission is critical - capture will not work without it
      onCameraPermissionFailed(error) {
        console.error("[PixaProof] Camera permission denied", error);
        document.getElementById("error").textContent =
          "Camera access denied. Please enable camera permissions and reload.";
      },

      // Location and sensor permissions are non-critical - capture still works
      onLocationPermissionFailed(error) {
        console.warn("[PixaProof] Location permission denied", error);
        locationPermissionFailed = true;
      },

      onSensorPermissionFailed(error) {
        console.warn("[PixaProof] Sensor permission denied", error);
        sensorPermissionFailed = true;
      },

      // ── Capture callbacks ────────────────────────────────────────────────────

      onCapture() {
        console.log("[PixaProof] Capturing…");
        document.getElementById("status").textContent = "Capturing…";
        document.getElementById("captureBtn").disabled = true;
      },

      onCaptureSuccess(blob) {
        console.log("[PixaProof] Capture successful", blob);
        document.getElementById("status").textContent = "Capture successful!";
        document.getElementById("captureBtn").disabled = false;

        // Option A: download the image locally
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = "pixaproof-capture.png";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);

        // Option B: upload to your backend for verification
        // const formData = new FormData();
        // formData.append("apiKey",  "YOUR_API_KEY");
        // formData.append("image",   blob, "capture.png");
        // formData.append("referenceId", "optional-ref-id");
        // fetch("{url}/api/v1/verification/image", {
        //   method: "POST",
        //   headers: { "Authorization": "Bearer YOUR_ACCESS_TOKEN" },
        //   body: formData,
        // }).then(r => r.json()).then(console.log);
      },

      onCaptureError(error) {
        console.error("[PixaProof] Capture error", error);
        document.getElementById("error").textContent = "Capture failed: " + error.message;
        document.getElementById("captureBtn").disabled = false;
      },
    });

    sdk.init();

    document.getElementById("captureBtn").addEventListener("click", () => {
      sdk.captureCamera().then(() => {
        sdk.closeCamera();
      });
    });
  </script>

</body>
</html>
```

---

## API Reference

### PixaProof Backend API

> **Security note**: All calls to the backend API that involve `client_id` or `client_secret` **must be made server-side**. Do not expose these credentials in browser code.

---

#### Get Client Access Token

Generates a short-lived OAuth2 Bearer token. Make this call from your **server**, then pass the resulting `access_token` to the frontend.

**Endpoint**

```
POST {url}/oauth2/token
Content-Type: application/x-www-form-urlencoded
```

**Request Parameters**

| Parameter | Required | Description |
|-----------|----------|-------------|
| `client_id` | Yes | Your client ID |
| `client_secret` | Yes | Your client secret |
| `grant_type` | Yes | Must be `client_credentials` |
| `scope` | Yes | Must be `api.read api.write` |

**Example Response**

```json
{
  "access_token": "eyJraWQiOiJ...",
  "scope": "api.write api.read",
  "token_type": "Bearer",
  "expires_in": 899
}
```

Pass `access_token` to your frontend and supply it as the `token` option when constructing `PixaProof`.

---

#### Submit Image for Verification

Uploads a captured image (the `Blob` from `onCaptureSuccess`) to the backend for verification.

**Endpoint**

```
POST {url}/api/v1/verification/image
Authorization: Bearer <access_token>
Content-Type: multipart/form-data
```

**Request Fields**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `apiKey` | Text | Yes | Your client API key |
| `image` | File | Yes | The image `Blob` returned by `onCaptureSuccess` |
| `referenceId` | Text | No | Optional identifier to correlate verification with your records |

**Example (browser fetch)**

```js
async function verifyImage(blob, accessToken) {
  const formData = new FormData();
  formData.append("apiKey",       "YOUR_API_KEY");
  formData.append("image",        blob, "capture.png");
  formData.append("referenceId",  "user-session-123"); // optional

  const response = await fetch("{url}/api/v1/verification/image", {
    method:  "POST",
    headers: { "Authorization": "Bearer " + accessToken },
    body:    formData,
  });

  return response.json();
}
```

##### Response Fields

| Field | Type | Values | Description |
|-------|------|--------|-------------|
| `overallResult` | string | `Pass` / `Fail` | Overall verification outcome |
| `verificationDetails.sourceOfOrigin` | string | `Mobile` / `Non-Mobile` / `null` | Whether the image was captured on a mobile device |
| `verificationDetails.integrityCheck` | string | `Pass` / `Fail` | Whether the integrity is intact and unmodified |
| `verificationDetails.livenessCapture` | string | `Pass` / `Fail` | Whether the image was captured live (not a replay or screenshot) |
| `geolocation` | object / `null` | - | Location at time of capture. `null` when `overallResult` is `Fail` |
| `geolocation.timezone` | string | - | IANA timezone name (e.g. `Asia/Kuala_Lumpur`) |
| `geolocation.datetime` | string | - | ISO 8601 capture timestamp |
| `geolocation.latitude` | string | - | Latitude coordinate |
| `geolocation.longitude` | string | - | Longitude coordinate |

##### Pass response

```json
{
  "overallResult": "Pass",
  "verificationDetails": {
    "sourceOfOrigin": "Mobile",
    "integrityCheck": "Pass",
    "livenessCapture": "Pass"
  },
  "geolocation": {
    "timezone": "Asia/Kuala_Lumpur",
    "datetime": "2026-02-24T02:15:44.413Z",
    "longitude": "101.586",
    "latitude": "3.047"
  }
}
```

##### Fail response

```json
{
  "overallResult": "Fail",
  "verificationDetails": {
    "sourceOfOrigin": null,
    "integrityCheck": "Fail",
    "livenessCapture": "Fail"
  },
  "geolocation": null
}
```

---

### SDK API

#### Constructor

```js
const sdk = new PixaProof(options);
```

#### Options

| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `apiUrl` | `string` | Yes | - | Base URL of your PixaProof backend |
| `apiKey` | `string` | Yes | - | Your client API key |
| `token` | `string` | Yes | - | OAuth2 Bearer access token (obtain server-side) |
| `elementId` | `string` | Yes | - | `id` of the DOM element the SDK renders into |
| `width` | `number` | No | `640` | Camera/canvas width in pixels |
| `height` | `number` | No | `480` | Camera/canvas height in pixels |
| `preferredCamera` | `"environment" \| "user" \| "any"` | No | `"environment"` | Preferred camera direction. The default prefers the rear camera and falls back if it cannot start. |
| `permissionUI` | `object` | No | - | Customise the permission prompt overlay (see below) |
| `cameraUI` | `object` | No | - | Customise the camera view elements (see below) |
| *(callbacks)* | `function` | No | - | See [Callbacks](#callbacks) below |

---

#### `permissionUI` Options

Each sub-key accepts a `styles` object (CSS property → value) and/or a `className` string.

| Sub-key | Description |
|---------|-------------|
| `overlay` | Full-screen backdrop behind the permission card |
| `container` | The card/box containing the message and button |
| `message` | Text area inside the card. Also accepts `html` (string) for rich content |
| `button` | The "Enable Permissions" button. Also accepts `text` and `loadingText` strings |

Pass `styles: {}` to strip all built-in default styles from an element.

---

#### `cameraUI` Options

Each sub-key accepts `styles` and/or `className`.

| Sub-key | Description |
|---------|-------------|
| `wrapper` | Outer wrapper around the camera card |
| `card` | Card element that holds the video |
| `videoWrapper` | Wrapper directly around the `<video>` element |
| `video` | The `<video>` element |
| `canvas` | The hidden `<canvas>` used for frame capture |
| `snapshot` | The snapshot `<img>` element (preview after capture) |

---

#### Methods

| Method | Signature | Description |
|--------|-----------|-------------|
| `init` | `() => Promise<void>` | Validates the API URL, renders the permission UI, requests camera/location/sensor permissions, and opens the camera stream. |
| `captureCamera` | `() => Promise<void>` | Captures the current video frame. Returns the result via `onCaptureSuccess`. |
| `closeCamera` | `() => Promise<void>` | Stops the camera stream and releases all resources. |

---

#### Callbacks

All callbacks are optional and are passed as properties of the `PixaProof` options object.

| Callback | Arguments | When it fires |
|----------|-----------|---------------|
| `onInit` | - | SDK initialization begins (before permissions are requested) |
| `onInitSuccess` | `authResponse` | Permissions granted and camera is open and ready |
| `onInitError` | `error` | Initialization failed (invalid URL, API error, etc.) |
| `onCameraPermissionFailed` | `error` | Camera access was denied - capture is not possible |
| `onLocationPermissionFailed` | `error` | Location access was denied - capture still works, location data will be absent |
| `onSensorPermissionFailed` | `error` | Motion sensor access was denied - capture still works, sensor data will be absent |
| `onCapture` | - | `captureCamera()` has been called and capture is in progress |
| `onCaptureSuccess` | `blob` | The image is ready as a `Blob` - upload this to `POST /api/v1/verification/image` |
| `onCaptureError` | `error` | Something went wrong during capture or encoding |

**`authResponse` shape**

```js
{
  publicKey: string,  // RSA public key from the backend
  version:   string,  // Encryption version identifier
}
```
