# Web pages and Helios Showcase

Use this guide when a hosted web application should run as a Helios Showcase mini program, when a native application needs to host a bridged WebView, or when code uses `window.RFIDReader`.

## Architecture

Helios Showcase owns the native SDK, platform permissions, and physical reader. Before loading the page, it injects a promise-and-event API:

```text
HTTPS web app → window.RFIDReader → native WebView bridge → app-scoped RFIDReader → Mark3
```

The built-in EZ Controller uses this contract. A custom page does not need Bluetooth, USB, or native SDK dependencies when it runs inside Showcase.

## Add a page to Showcase

1. Deploy the page at a trusted HTTPS URL that works in a mobile WebView.
2. In Helios Showcase, connect the Mark3 reader first.
3. Open **Mini Programs**, choose **Add**, enter a name and the HTTPS URL, then launch it.
4. In the page, detect the bridge, subscribe to events, call `getState()`, and enable reader controls only when connected and ready.
5. Stop inventory and remove listeners when the page is hidden or unloaded.

The Apple and Android Showcase applications also resolve this deep-link shape:

```text
rfidreader://miniprogram?id=my.inventory&name=Inventory&url=https%3A%2F%2Fexample.com%2Frfid
```

Percent-encode every query value. Treat deep links from outside the app as untrusted input and validate the target URL before loading it.

## Detect the injected bridge

The script is normally injected at document start. Handle both immediate and delayed availability:

```js
function whenRFIDReaderReady(callback) {
  if (window.RFIDReader?.__installed) {
    callback(window.RFIDReader);
    return;
  }

  window.addEventListener(
    "rfidreaderready",
    () => callback(window.RFIDReader),
    { once: true },
  );
}
```

Do not detect Showcase from its user agent. The bridge object and `rfidreaderready` event are the capability contract.

## Minimal inventory page

Subscribe before starting inventory so the first events are not lost. A fulfilled command promise means the native bridge accepted the call; render scanning state from events.

```js
let reader;
let scanning = false;
const unsubscribe = [];

whenRFIDReaderReady(async (api) => {
  reader = api;

  unsubscribe.push(
    api.on("ready", (info) => renderReader(info)),
    api.on("scanStart", () => { scanning = true; renderScanning(true); }),
    api.on("scanStop", () => { scanning = false; renderScanning(false); }),
    api.on("tag", (tag) => upsertTag(tag.epc, tag)),
    api.on("disconnect", () => renderDisconnected()),
    api.on("error", (error) => renderError(error?.message ?? String(error))),
  );

  const state = await api.getState();
  renderConnection(state);
});

async function startInventory() {
  if (!reader) throw new Error("Helios bridge is not ready");
  const state = await reader.getState();
  if (!state.isConnected) throw new Error("Connect a reader in Helios Showcase first");

  await reader.startAsyncInventory({
    metadata: ["returnRSSI", "returnTimestamp", "returnReadCount"],
  });
}

async function stopInventory() {
  if (reader && scanning) await reader.stopAsyncInventory();
}

window.addEventListener("pagehide", () => {
  void stopInventory();
  unsubscribe.splice(0).forEach((off) => off());
});
```

Implement the render functions inside the project's existing state/UI layer. De-duplicate inventory by EPC and retain read count, last-seen time, RSSI, and antenna where the workflow needs them.

## Public JavaScript contract

All supported commands return promises. Methods accept one optional object argument.

| Area | Methods and arguments |
| --- | --- |
| State | `getState()` → `{ isConnected, isScanning, model?, name?, serialNumber?, version? }` |
| Discovery | `startDeviceScan()`, `stopDeviceScan()`; observe `deviceFound` |
| Connection | `connect({ model, name?, transport })`, `disconnect()` |
| Inventory | `startAsyncInventory({ powerSave?, metadata?, password?, tagSingulationFlag?, tagSingulationData?, memoryBank?, readAddress?, wordCount? })`, `stopAsyncInventory()` |
| Single/read | `singleTagInventory({ timeout, metadata?, tagSingulationFlag?, tagSingulationData? })`, `readTagMemoryBank({ timeout, memoryBank, readAddress, wordCount, ... })` |
| Identity | `getVersion()`, `getSerialNumber()`, `getDeviceName()`, `getModel()`, `getIPv4()`, `getMac()` |
| RF configuration | `getRegion()`, `setRegion({ region, permanent? })`, antenna, session, target, RF mode, Q, and reader-configuration getters/setters |
| Advanced | tag write/lock/kill, raw command, reboot, factory reset, and OTA methods |

Read-only calls and inventory are the default. Use tag writes, permanent RF changes, reboot, reset, raw commands, and OTA only after explicit user confirmation and with operation-specific validation.

Supported event names are:

| Event | Payload/use |
| --- | --- |
| `deviceFound` | `{ identifier, name, ... }` from host discovery |
| `connect` | Transport connected; do not send commands until `ready` |
| `ready` | Reader identity is available and commands may begin |
| `disconnect` | Clear connected and scanning UI state |
| `scanStart` / `scanStop` | Authoritative inventory state |
| `tag` | `{ epc, epcLen, pc?, rssi?, antennaID?, frequency?, timestamp?, phase?, tagDataLen? }` |
| `message` | Typed result of a configuration/identity command; discriminate on `payload.type` |
| `error` | `{ message }` |
| `raw` | Uppercase hexadecimal response frame for protocol-level clients |

Register listeners with `RFIDReader.on(event, callback)`. It returns an unsubscribe function. `off(event, callback)` and `once(event, callback)` are also available.

## Reader ownership modes

### Reuse the Showcase reader: recommended

Connect in the native Showcase UI, then open the page. `getState()` mirrors that shared reader. Calling `connect()` while the host already owns a reader reuses it rather than replacing it.

### Let the page discover and connect

This works only when the host supplied a device scanner and platform permissions were granted:

```js
const devices = new Map();
const offFound = RFIDReader.on("deviceFound", (device) => {
  devices.set(device.identifier, device);
});

await RFIDReader.startDeviceScan();
// Let the user select one device, then stop discovery before connecting.
await RFIDReader.stopDeviceScan();
offFound();

await RFIDReader.connect({
  model: "MARK3-TT7",
  name: selected.name,
  transport: { type: "ble", identifier: selected.identifier },
});
```

Apple and Android bridge transports accept `ble` and `wifi`. A Flutter host may additionally support `usb`; verify the pinned package before presenting it.

## Native host setup

Install the bridge before loading any remote content and point it at the application's single reader.

Apple:

```swift
let bridge = RFIDReaderWebBridge(
    reader: { appState.reader },
    additionalDelegates: [readerManager],
    onReaderCreated: { appState.reader = $0 },
    deviceScanner: appScanner
)
bridge.install(on: webView)
webView.load(URLRequest(url: trustedURL))
```

Android:

```kotlin
val bridge = RFIDReaderWebBridge(
    context = applicationContext,
    readerProvider = { appState.reader },
    additionalDelegates = listOf(readerManager),
    onReaderCreated = { appState.reader = it },
    deviceScanner = appScanner,
)
bridge.installOn(webView)
webView.loadUrl(trustedUrl)
```

Flutter, when the pinned revision exposes these symbols:

```dart
final controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted);
final bridge = HeliosWebBridge(
  reader: () => appReader,
  scanner: deviceScanner,
  onReaderCreated: (reader) => appReader = reader,
);
HeliosWebView.attach(controller, bridge);
await controller.loadRequest(trustedUri);
```

Retain the bridge for the screen lifetime. Uninstall/dispose it on teardown; do not disconnect a shared reader merely because the web page closed.

## WebView security requirements

The injected object can inventory, configure, write tags, and perform destructive reader operations. Treat it as privileged:

- load only allowlisted HTTPS origins;
- block or open external navigation outside the bridged WebView;
- never enable the bridge for advertisements, arbitrary user browsing, or untrusted iframes;
- keep file/content access disabled unless a reviewed local bundle requires it;
- use a strict Content Security Policy on the web app;
- never place access passwords, Wi-Fi credentials, or firmware URLs in page source or logs;
- display user confirmation before tag writes, permanent settings, reset, reboot, or OTA.

## Test without hardware

Mock the bridge at the UI boundary. Cover immediate and delayed bridge readiness, disconnected state, a tag burst, duplicate EPCs, scan stop, native errors, and listener cleanup.

```js
function installMockRFIDReader() {
  const listeners = new Map();
  const emit = (event, data) => (listeners.get(event) ?? []).forEach((fn) => fn(data));

  window.RFIDReader = {
    __installed: true,
    async getState() { return { isConnected: true, isScanning: false, model: "MARK3-TT7" }; },
    async startAsyncInventory() { queueMicrotask(() => emit("scanStart")); },
    async stopAsyncInventory() { queueMicrotask(() => emit("scanStop")); },
    on(event, callback) {
      const callbacks = listeners.get(event) ?? [];
      callbacks.push(callback);
      listeners.set(event, callbacks);
      return () => listeners.set(event, callbacks.filter((item) => item !== callback));
    },
  };
  window.dispatchEvent(new Event("rfidreaderready"));
  return { emit };
}
```

Browser tests prove UI and event handling. A physical Mark3 reader is still required to verify permissions, transport, readiness, RF settings, read-zone performance, and tag operations.
