@proveanything/smartlinks 1.11.0 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/API_SUMMARY.md +1 -1
- package/dist/docs/app-manifest.md +6 -6
- package/dist/docs/mobile-admin-container.md +80 -19
- package/docs/API_SUMMARY.md +1 -1
- package/docs/app-manifest.md +6 -6
- package/docs/mobile-admin-container.md +80 -19
- package/package.json +1 -1
- package/docs/scanner-container.md +0 -556
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -103,8 +103,8 @@ The manifest is loaded automatically by the platform for every collection page.
|
|
|
103
103
|
{
|
|
104
104
|
"name": "WarehousePickContainer",
|
|
105
105
|
"description": "In-field operator admin surface.",
|
|
106
|
-
"
|
|
107
|
-
"
|
|
106
|
+
"capabilities": ["nfc", "qr"],
|
|
107
|
+
"offline": true
|
|
108
108
|
}
|
|
109
109
|
]
|
|
110
110
|
},
|
|
@@ -236,8 +236,8 @@ See [mobile-admin-container.md](mobile-admin-container.md) for the `AdminMobileH
|
|
|
236
236
|
{
|
|
237
237
|
"name": "WarehousePickContainer",
|
|
238
238
|
"description": "Pick orders by scanning NFC tags",
|
|
239
|
-
"
|
|
240
|
-
"
|
|
239
|
+
"capabilities": ["nfc", "qr"],
|
|
240
|
+
"offline": true
|
|
241
241
|
}
|
|
242
242
|
]
|
|
243
243
|
}
|
|
@@ -250,8 +250,8 @@ See [mobile-admin-container.md](mobile-admin-container.md) for the `AdminMobileH
|
|
|
250
250
|
| `files.css` | CSS bundle path — set to `null` if no styles |
|
|
251
251
|
| `components[].name` | Exported component name (must match the UMD bundle export) |
|
|
252
252
|
| `components[].description` | Shown in the mobile launcher's app picker |
|
|
253
|
-
| `components[].
|
|
254
|
-
| `components[].
|
|
253
|
+
| `components[].capabilities` | Hardware capabilities this component needs or can use. See [capability list](mobile-admin-container.md#hardware-capabilities--the-capability-matrix). |
|
|
254
|
+
| `components[].offline` | Set to `true` if this component queues writes locally and needs offline sync support. |
|
|
255
255
|
#### `linkable`
|
|
256
256
|
|
|
257
257
|
Static deep-linkable states built into the app — fixed routes that exist regardless of per-collection content. Declared once at build time.
|
|
@@ -16,12 +16,13 @@ This document describes how to build a **Mobile Admin Container** — a SmartLin
|
|
|
16
16
|
4. [Hardware Capabilities & the Capability Matrix](#hardware-capabilities--the-capability-matrix)
|
|
17
17
|
5. [Capacitor Plugin Baseline](#capacitor-plugin-baseline)
|
|
18
18
|
6. [Manifest Declaration](#manifest-declaration)
|
|
19
|
-
7. [
|
|
20
|
-
8. [
|
|
21
|
-
9. [
|
|
22
|
-
10. [
|
|
23
|
-
11. [
|
|
24
|
-
12. [
|
|
19
|
+
7. [Launcher Discovery](#launcher-discovery)
|
|
20
|
+
8. [Event Stream](#event-stream)
|
|
21
|
+
9. [Error Handling](#error-handling)
|
|
22
|
+
10. [Lifecycle](#lifecycle)
|
|
23
|
+
11. [Build & Bundle Requirements](#build--bundle-requirements)
|
|
24
|
+
12. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
|
|
25
|
+
13. [Best Practices](#best-practices)
|
|
25
26
|
|
|
26
27
|
---
|
|
27
28
|
|
|
@@ -106,7 +107,8 @@ interface AdminMobileHostContext {
|
|
|
106
107
|
network: { isOnline: () => boolean }
|
|
107
108
|
device: { info: () => Promise<{ model: string; platform: string }> }
|
|
108
109
|
|
|
109
|
-
//
|
|
110
|
+
// Informational host version — use for logging/diagnostics, not feature detection.
|
|
111
|
+
// For feature detection prefer existence checks: 'requestNfcTap' in host.actions
|
|
110
112
|
_version: number
|
|
111
113
|
}
|
|
112
114
|
```
|
|
@@ -114,7 +116,15 @@ interface AdminMobileHostContext {
|
|
|
114
116
|
### Contract rules
|
|
115
117
|
|
|
116
118
|
1. **Check `host.hardware.X` before calling `host.actions.X`.** Calls to unavailable capabilities reject with `HostCapabilityUnavailableError`.
|
|
117
|
-
2. **Do not call `initializeApi
|
|
119
|
+
2. **Do not call `initializeApi`, and do not use top-level SDK imports for API calls.** `host.SL` is already configured with the right `baseURL`, auth, and logger. Code that does `import * as SL from "@proveanything/smartlinks"` and calls `SL.attestation.create(...)` will silently hit the wrong base URL — the top-level import is uninitialised in the launcher's runtime and produces no loud error. Always go through `host.SL`:
|
|
120
|
+
```typescript
|
|
121
|
+
// ❌ Silent footgun — uninitialised SDK, wrong baseURL
|
|
122
|
+
import * as SL from '@proveanything/smartlinks'
|
|
123
|
+
await SL.attestation.create(...)
|
|
124
|
+
|
|
125
|
+
// ✅ Correct
|
|
126
|
+
await host.SL.attestation.create(...)
|
|
127
|
+
```
|
|
118
128
|
3. **Do not access `window.SmartlinksScanner` or `window.Capacitor` directly.** The host wraps both; direct access produces containers that only work on one shell.
|
|
119
129
|
4. **Externalise all shared deps** (React, ReactDOM, SL, Radix, etc.) — the launcher provides them as globals. See [Build & Bundle Requirements](#build--bundle-requirements).
|
|
120
130
|
|
|
@@ -129,6 +139,36 @@ if (host.hardware.nfc) {
|
|
|
129
139
|
}
|
|
130
140
|
```
|
|
131
141
|
|
|
142
|
+
### `host.ui` — native helpers vs. your own components
|
|
143
|
+
|
|
144
|
+
`host.ui.setHeaderTitle()` and `host.ui.navigateBack()` are **host-only** — there is no in-container equivalent. Call them via `host.ui` or omit them.
|
|
145
|
+
|
|
146
|
+
`host.ui.toast()` and `host.ui.haptic()` are **optional conveniences**. Use them when you want native system feedback. When rendering in Storybook, unit tests, or a plain browser tab, your own `<Toaster />` is a perfectly valid substitute — you do not need to stub the entire `host.ui` surface just to get toast notifications.
|
|
147
|
+
|
|
148
|
+
**Stub pattern for testing and Storybook:**
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const stubHost: Partial<AdminMobileHostContext> = {
|
|
152
|
+
hardware: { nfc: false, rfid: false, qr: true, camera: true, keyboard: false },
|
|
153
|
+
actions: {
|
|
154
|
+
requestQrScan: async () => 'STUB_QR_CODE',
|
|
155
|
+
requestNfcTap: async () => ({ uid: 'STUB_UID' }),
|
|
156
|
+
requestCameraPhoto: async () => new Blob(),
|
|
157
|
+
share: async () => {},
|
|
158
|
+
clipboard: { read: async () => '', write: async () => {} },
|
|
159
|
+
},
|
|
160
|
+
ui: {
|
|
161
|
+
toast: (opts) => console.log('[stub toast]', opts.title),
|
|
162
|
+
haptic: () => {},
|
|
163
|
+
setHeaderTitle: (t) => { if (t) document.title = t },
|
|
164
|
+
navigateBack: () => history.back(),
|
|
165
|
+
},
|
|
166
|
+
network: { isOnline: () => true },
|
|
167
|
+
user: { isAdmin: true, displayName: 'Test User', email: 'test@example.com' },
|
|
168
|
+
SL: undefined as any, // replace with your test SL instance
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
132
172
|
---
|
|
133
173
|
|
|
134
174
|
## Hardware Capabilities & the Capability Matrix
|
|
@@ -148,7 +188,6 @@ Containers declare which capabilities they need (or can use) in the manifest `ca
|
|
|
148
188
|
| `"camera"` | Photo capture, gallery picker |
|
|
149
189
|
| `"keyboard"` | Physical hardware trigger/action buttons |
|
|
150
190
|
| `"geolocation"` | GPS coordinates |
|
|
151
|
-
| `"offline-queue"` | Container needs local write queuing + sync |
|
|
152
191
|
| `"push"` | Remote push notifications (FCM/APNs) |
|
|
153
192
|
|
|
154
193
|
The full hardware capability matrix by host:
|
|
@@ -196,7 +235,7 @@ The custom Kotlin shell and both Capacitor shells ship the same baseline plugin
|
|
|
196
235
|
| `@capgo/capacitor-nfc` | `"nfc"` |
|
|
197
236
|
| `@capacitor/geolocation` | `"geolocation"` |
|
|
198
237
|
| `@capacitor/push-notifications` | `"push"` |
|
|
199
|
-
| `@capacitor/filesystem` | *(
|
|
238
|
+
| `@capacitor/filesystem` | *(used when `offline: true`; bundle it in)* |
|
|
200
239
|
|
|
201
240
|
### Tier 3 — custom-android only (not Capacitor)
|
|
202
241
|
|
|
@@ -231,8 +270,8 @@ Declare the bundle under the top-level `mobileAdmin` key in `app.manifest.json`.
|
|
|
231
270
|
{
|
|
232
271
|
"name": "WarehousePickContainer",
|
|
233
272
|
"description": "Pick orders by scanning NFC tags",
|
|
234
|
-
"
|
|
235
|
-
"
|
|
273
|
+
"capabilities": ["nfc", "qr"],
|
|
274
|
+
"offline": true
|
|
236
275
|
}
|
|
237
276
|
]
|
|
238
277
|
}
|
|
@@ -247,8 +286,29 @@ A `mobileAdmin` bundle can export multiple components targeting different operat
|
|
|
247
286
|
|-------|------|-------------|
|
|
248
287
|
| `name` | string | Exported component name (must match the UMD bundle export) |
|
|
249
288
|
| `description` | string | Shown in the mobile launcher's app picker |
|
|
250
|
-
| `
|
|
251
|
-
| `
|
|
289
|
+
| `capabilities` | string[] | Hardware capabilities this component needs or can use. See table above. |
|
|
290
|
+
| `offline` | boolean | Set to `true` if this component queues writes locally and needs offline sync support. |
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Launcher Discovery
|
|
295
|
+
|
|
296
|
+
The SmartLinks Mobile launcher loads your `mobileAdmin` bundle through the platform's app registry — you do not reference the manifest URL directly.
|
|
297
|
+
|
|
298
|
+
**How it works:**
|
|
299
|
+
|
|
300
|
+
1. A collection admin enables the app for a collection in the SmartLinks admin console.
|
|
301
|
+
2. On startup (and periodically), the launcher fetches `app.manifest.json` for every enabled app in that collection via the SmartLinks platform API. These requests are authenticated with the launcher's operator session.
|
|
302
|
+
3. The launcher reads `mobileAdmin.components[]`, evaluates each component's `capabilities` against the current device's hardware flags, and shows matching components in the app picker.
|
|
303
|
+
4. When the operator opens a component, the launcher resolves `mobileAdmin.files.js.umd` against the app's CDN base, fetches the bundle, and mounts the component with its `host` prop.
|
|
304
|
+
|
|
305
|
+
**File URL resolution:** `files.js.umd` / `files.js.esm` are paths relative to your app's CDN root (set when the app version is published). Provide relative paths — the launcher resolves them; do not hardcode absolute URLs.
|
|
306
|
+
|
|
307
|
+
**Auth:** Manifest and bundle fetches use the launcher's session token. Your container is already authenticated via `host.user` and `host.SL` — do not add additional auth headers.
|
|
308
|
+
|
|
309
|
+
**On load failure:** If the bundle returns a non-2xx response, the launcher shows a user-visible error and logs it — it does not crash the full launcher. If `app.manifest.json` itself cannot be fetched, the app is silently excluded from the picker for that session.
|
|
310
|
+
|
|
311
|
+
> To debug a missing or stale manifest, confirm in the SmartLinks admin console that the app version is published and that the `mobileAdmin` key is present in the uploaded manifest.
|
|
252
312
|
|
|
253
313
|
---
|
|
254
314
|
|
|
@@ -318,7 +378,7 @@ try {
|
|
|
318
378
|
|-------|------|------------|
|
|
319
379
|
| mount | User opens your container | Subscribe to events, start readers |
|
|
320
380
|
| unmount | User backs out / app backgrounded > 30s | Unsubscribe; persist in-flight state |
|
|
321
|
-
| `lifecycle: 'offline'` | Network lost | Switch to offline
|
|
381
|
+
| `lifecycle: 'offline'` | Network lost | Switch to offline mode; queue writes locally |
|
|
322
382
|
| `lifecycle: 'online'` | Network restored | Flush queued writes |
|
|
323
383
|
| `lifecycle: 'pause'` | App backgrounded | Pause readers to save battery |
|
|
324
384
|
| `lifecycle: 'resume'` | App foregrounded | Resubscribe; refresh stale data |
|
|
@@ -459,9 +519,10 @@ export const MOBILE_ADMIN_MANIFEST = {
|
|
|
459
519
|
|
|
460
520
|
- **Always check `host.hardware.X` before calling `host.actions.X`** — never assume a capability is available.
|
|
461
521
|
- **Always wrap action calls in try/catch** — handle `HostPermissionDeniedError`, `HostTimeoutError`, and `HostCapabilityUnavailableError`.
|
|
462
|
-
- **Use `host.ui.
|
|
522
|
+
- **Use `host.ui.setHeaderTitle` and `host.ui.navigateBack`** for header integration — these are host-only and have no in-container equivalent.
|
|
523
|
+
- **`host.ui.toast` and `host.ui.haptic` are optional** — use them for native feedback, or fall back to your own `<Toaster />` when testing in isolation.
|
|
463
524
|
- **Use `host.events.subscribe` for lifecycle events** — `'offline'`/`'online'`/`'pause'`/`'resume'` fire consistently on every host.
|
|
464
|
-
- **Never call `initializeApi
|
|
525
|
+
- **Never call `initializeApi`, never use top-level SDK imports for API calls** — `host.SL` is already configured. `SL.method()` instead of `host.SL.method()` silently uses the wrong baseURL.
|
|
465
526
|
- **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
|
|
466
|
-
- **Declare `offline
|
|
467
|
-
- **Use `host.
|
|
527
|
+
- **Declare `offline: true`** on a component if it queues writes locally — this signals the launcher to provision offline sync support.
|
|
528
|
+
- **Use `'methodName' in host.actions`** to feature-detect new host capabilities rather than comparing `host._version`. The version is informational only.
|
package/docs/API_SUMMARY.md
CHANGED
package/docs/app-manifest.md
CHANGED
|
@@ -103,8 +103,8 @@ The manifest is loaded automatically by the platform for every collection page.
|
|
|
103
103
|
{
|
|
104
104
|
"name": "WarehousePickContainer",
|
|
105
105
|
"description": "In-field operator admin surface.",
|
|
106
|
-
"
|
|
107
|
-
"
|
|
106
|
+
"capabilities": ["nfc", "qr"],
|
|
107
|
+
"offline": true
|
|
108
108
|
}
|
|
109
109
|
]
|
|
110
110
|
},
|
|
@@ -236,8 +236,8 @@ See [mobile-admin-container.md](mobile-admin-container.md) for the `AdminMobileH
|
|
|
236
236
|
{
|
|
237
237
|
"name": "WarehousePickContainer",
|
|
238
238
|
"description": "Pick orders by scanning NFC tags",
|
|
239
|
-
"
|
|
240
|
-
"
|
|
239
|
+
"capabilities": ["nfc", "qr"],
|
|
240
|
+
"offline": true
|
|
241
241
|
}
|
|
242
242
|
]
|
|
243
243
|
}
|
|
@@ -250,8 +250,8 @@ See [mobile-admin-container.md](mobile-admin-container.md) for the `AdminMobileH
|
|
|
250
250
|
| `files.css` | CSS bundle path — set to `null` if no styles |
|
|
251
251
|
| `components[].name` | Exported component name (must match the UMD bundle export) |
|
|
252
252
|
| `components[].description` | Shown in the mobile launcher's app picker |
|
|
253
|
-
| `components[].
|
|
254
|
-
| `components[].
|
|
253
|
+
| `components[].capabilities` | Hardware capabilities this component needs or can use. See [capability list](mobile-admin-container.md#hardware-capabilities--the-capability-matrix). |
|
|
254
|
+
| `components[].offline` | Set to `true` if this component queues writes locally and needs offline sync support. |
|
|
255
255
|
#### `linkable`
|
|
256
256
|
|
|
257
257
|
Static deep-linkable states built into the app — fixed routes that exist regardless of per-collection content. Declared once at build time.
|
|
@@ -16,12 +16,13 @@ This document describes how to build a **Mobile Admin Container** — a SmartLin
|
|
|
16
16
|
4. [Hardware Capabilities & the Capability Matrix](#hardware-capabilities--the-capability-matrix)
|
|
17
17
|
5. [Capacitor Plugin Baseline](#capacitor-plugin-baseline)
|
|
18
18
|
6. [Manifest Declaration](#manifest-declaration)
|
|
19
|
-
7. [
|
|
20
|
-
8. [
|
|
21
|
-
9. [
|
|
22
|
-
10. [
|
|
23
|
-
11. [
|
|
24
|
-
12. [
|
|
19
|
+
7. [Launcher Discovery](#launcher-discovery)
|
|
20
|
+
8. [Event Stream](#event-stream)
|
|
21
|
+
9. [Error Handling](#error-handling)
|
|
22
|
+
10. [Lifecycle](#lifecycle)
|
|
23
|
+
11. [Build & Bundle Requirements](#build--bundle-requirements)
|
|
24
|
+
12. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
|
|
25
|
+
13. [Best Practices](#best-practices)
|
|
25
26
|
|
|
26
27
|
---
|
|
27
28
|
|
|
@@ -106,7 +107,8 @@ interface AdminMobileHostContext {
|
|
|
106
107
|
network: { isOnline: () => boolean }
|
|
107
108
|
device: { info: () => Promise<{ model: string; platform: string }> }
|
|
108
109
|
|
|
109
|
-
//
|
|
110
|
+
// Informational host version — use for logging/diagnostics, not feature detection.
|
|
111
|
+
// For feature detection prefer existence checks: 'requestNfcTap' in host.actions
|
|
110
112
|
_version: number
|
|
111
113
|
}
|
|
112
114
|
```
|
|
@@ -114,7 +116,15 @@ interface AdminMobileHostContext {
|
|
|
114
116
|
### Contract rules
|
|
115
117
|
|
|
116
118
|
1. **Check `host.hardware.X` before calling `host.actions.X`.** Calls to unavailable capabilities reject with `HostCapabilityUnavailableError`.
|
|
117
|
-
2. **Do not call `initializeApi
|
|
119
|
+
2. **Do not call `initializeApi`, and do not use top-level SDK imports for API calls.** `host.SL` is already configured with the right `baseURL`, auth, and logger. Code that does `import * as SL from "@proveanything/smartlinks"` and calls `SL.attestation.create(...)` will silently hit the wrong base URL — the top-level import is uninitialised in the launcher's runtime and produces no loud error. Always go through `host.SL`:
|
|
120
|
+
```typescript
|
|
121
|
+
// ❌ Silent footgun — uninitialised SDK, wrong baseURL
|
|
122
|
+
import * as SL from '@proveanything/smartlinks'
|
|
123
|
+
await SL.attestation.create(...)
|
|
124
|
+
|
|
125
|
+
// ✅ Correct
|
|
126
|
+
await host.SL.attestation.create(...)
|
|
127
|
+
```
|
|
118
128
|
3. **Do not access `window.SmartlinksScanner` or `window.Capacitor` directly.** The host wraps both; direct access produces containers that only work on one shell.
|
|
119
129
|
4. **Externalise all shared deps** (React, ReactDOM, SL, Radix, etc.) — the launcher provides them as globals. See [Build & Bundle Requirements](#build--bundle-requirements).
|
|
120
130
|
|
|
@@ -129,6 +139,36 @@ if (host.hardware.nfc) {
|
|
|
129
139
|
}
|
|
130
140
|
```
|
|
131
141
|
|
|
142
|
+
### `host.ui` — native helpers vs. your own components
|
|
143
|
+
|
|
144
|
+
`host.ui.setHeaderTitle()` and `host.ui.navigateBack()` are **host-only** — there is no in-container equivalent. Call them via `host.ui` or omit them.
|
|
145
|
+
|
|
146
|
+
`host.ui.toast()` and `host.ui.haptic()` are **optional conveniences**. Use them when you want native system feedback. When rendering in Storybook, unit tests, or a plain browser tab, your own `<Toaster />` is a perfectly valid substitute — you do not need to stub the entire `host.ui` surface just to get toast notifications.
|
|
147
|
+
|
|
148
|
+
**Stub pattern for testing and Storybook:**
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const stubHost: Partial<AdminMobileHostContext> = {
|
|
152
|
+
hardware: { nfc: false, rfid: false, qr: true, camera: true, keyboard: false },
|
|
153
|
+
actions: {
|
|
154
|
+
requestQrScan: async () => 'STUB_QR_CODE',
|
|
155
|
+
requestNfcTap: async () => ({ uid: 'STUB_UID' }),
|
|
156
|
+
requestCameraPhoto: async () => new Blob(),
|
|
157
|
+
share: async () => {},
|
|
158
|
+
clipboard: { read: async () => '', write: async () => {} },
|
|
159
|
+
},
|
|
160
|
+
ui: {
|
|
161
|
+
toast: (opts) => console.log('[stub toast]', opts.title),
|
|
162
|
+
haptic: () => {},
|
|
163
|
+
setHeaderTitle: (t) => { if (t) document.title = t },
|
|
164
|
+
navigateBack: () => history.back(),
|
|
165
|
+
},
|
|
166
|
+
network: { isOnline: () => true },
|
|
167
|
+
user: { isAdmin: true, displayName: 'Test User', email: 'test@example.com' },
|
|
168
|
+
SL: undefined as any, // replace with your test SL instance
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
132
172
|
---
|
|
133
173
|
|
|
134
174
|
## Hardware Capabilities & the Capability Matrix
|
|
@@ -148,7 +188,6 @@ Containers declare which capabilities they need (or can use) in the manifest `ca
|
|
|
148
188
|
| `"camera"` | Photo capture, gallery picker |
|
|
149
189
|
| `"keyboard"` | Physical hardware trigger/action buttons |
|
|
150
190
|
| `"geolocation"` | GPS coordinates |
|
|
151
|
-
| `"offline-queue"` | Container needs local write queuing + sync |
|
|
152
191
|
| `"push"` | Remote push notifications (FCM/APNs) |
|
|
153
192
|
|
|
154
193
|
The full hardware capability matrix by host:
|
|
@@ -196,7 +235,7 @@ The custom Kotlin shell and both Capacitor shells ship the same baseline plugin
|
|
|
196
235
|
| `@capgo/capacitor-nfc` | `"nfc"` |
|
|
197
236
|
| `@capacitor/geolocation` | `"geolocation"` |
|
|
198
237
|
| `@capacitor/push-notifications` | `"push"` |
|
|
199
|
-
| `@capacitor/filesystem` | *(
|
|
238
|
+
| `@capacitor/filesystem` | *(used when `offline: true`; bundle it in)* |
|
|
200
239
|
|
|
201
240
|
### Tier 3 — custom-android only (not Capacitor)
|
|
202
241
|
|
|
@@ -231,8 +270,8 @@ Declare the bundle under the top-level `mobileAdmin` key in `app.manifest.json`.
|
|
|
231
270
|
{
|
|
232
271
|
"name": "WarehousePickContainer",
|
|
233
272
|
"description": "Pick orders by scanning NFC tags",
|
|
234
|
-
"
|
|
235
|
-
"
|
|
273
|
+
"capabilities": ["nfc", "qr"],
|
|
274
|
+
"offline": true
|
|
236
275
|
}
|
|
237
276
|
]
|
|
238
277
|
}
|
|
@@ -247,8 +286,29 @@ A `mobileAdmin` bundle can export multiple components targeting different operat
|
|
|
247
286
|
|-------|------|-------------|
|
|
248
287
|
| `name` | string | Exported component name (must match the UMD bundle export) |
|
|
249
288
|
| `description` | string | Shown in the mobile launcher's app picker |
|
|
250
|
-
| `
|
|
251
|
-
| `
|
|
289
|
+
| `capabilities` | string[] | Hardware capabilities this component needs or can use. See table above. |
|
|
290
|
+
| `offline` | boolean | Set to `true` if this component queues writes locally and needs offline sync support. |
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Launcher Discovery
|
|
295
|
+
|
|
296
|
+
The SmartLinks Mobile launcher loads your `mobileAdmin` bundle through the platform's app registry — you do not reference the manifest URL directly.
|
|
297
|
+
|
|
298
|
+
**How it works:**
|
|
299
|
+
|
|
300
|
+
1. A collection admin enables the app for a collection in the SmartLinks admin console.
|
|
301
|
+
2. On startup (and periodically), the launcher fetches `app.manifest.json` for every enabled app in that collection via the SmartLinks platform API. These requests are authenticated with the launcher's operator session.
|
|
302
|
+
3. The launcher reads `mobileAdmin.components[]`, evaluates each component's `capabilities` against the current device's hardware flags, and shows matching components in the app picker.
|
|
303
|
+
4. When the operator opens a component, the launcher resolves `mobileAdmin.files.js.umd` against the app's CDN base, fetches the bundle, and mounts the component with its `host` prop.
|
|
304
|
+
|
|
305
|
+
**File URL resolution:** `files.js.umd` / `files.js.esm` are paths relative to your app's CDN root (set when the app version is published). Provide relative paths — the launcher resolves them; do not hardcode absolute URLs.
|
|
306
|
+
|
|
307
|
+
**Auth:** Manifest and bundle fetches use the launcher's session token. Your container is already authenticated via `host.user` and `host.SL` — do not add additional auth headers.
|
|
308
|
+
|
|
309
|
+
**On load failure:** If the bundle returns a non-2xx response, the launcher shows a user-visible error and logs it — it does not crash the full launcher. If `app.manifest.json` itself cannot be fetched, the app is silently excluded from the picker for that session.
|
|
310
|
+
|
|
311
|
+
> To debug a missing or stale manifest, confirm in the SmartLinks admin console that the app version is published and that the `mobileAdmin` key is present in the uploaded manifest.
|
|
252
312
|
|
|
253
313
|
---
|
|
254
314
|
|
|
@@ -318,7 +378,7 @@ try {
|
|
|
318
378
|
|-------|------|------------|
|
|
319
379
|
| mount | User opens your container | Subscribe to events, start readers |
|
|
320
380
|
| unmount | User backs out / app backgrounded > 30s | Unsubscribe; persist in-flight state |
|
|
321
|
-
| `lifecycle: 'offline'` | Network lost | Switch to offline
|
|
381
|
+
| `lifecycle: 'offline'` | Network lost | Switch to offline mode; queue writes locally |
|
|
322
382
|
| `lifecycle: 'online'` | Network restored | Flush queued writes |
|
|
323
383
|
| `lifecycle: 'pause'` | App backgrounded | Pause readers to save battery |
|
|
324
384
|
| `lifecycle: 'resume'` | App foregrounded | Resubscribe; refresh stale data |
|
|
@@ -459,9 +519,10 @@ export const MOBILE_ADMIN_MANIFEST = {
|
|
|
459
519
|
|
|
460
520
|
- **Always check `host.hardware.X` before calling `host.actions.X`** — never assume a capability is available.
|
|
461
521
|
- **Always wrap action calls in try/catch** — handle `HostPermissionDeniedError`, `HostTimeoutError`, and `HostCapabilityUnavailableError`.
|
|
462
|
-
- **Use `host.ui.
|
|
522
|
+
- **Use `host.ui.setHeaderTitle` and `host.ui.navigateBack`** for header integration — these are host-only and have no in-container equivalent.
|
|
523
|
+
- **`host.ui.toast` and `host.ui.haptic` are optional** — use them for native feedback, or fall back to your own `<Toaster />` when testing in isolation.
|
|
463
524
|
- **Use `host.events.subscribe` for lifecycle events** — `'offline'`/`'online'`/`'pause'`/`'resume'` fire consistently on every host.
|
|
464
|
-
- **Never call `initializeApi
|
|
525
|
+
- **Never call `initializeApi`, never use top-level SDK imports for API calls** — `host.SL` is already configured. `SL.method()` instead of `host.SL.method()` silently uses the wrong baseURL.
|
|
465
526
|
- **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
|
|
466
|
-
- **Declare `offline
|
|
467
|
-
- **Use `host.
|
|
527
|
+
- **Declare `offline: true`** on a component if it queues writes locally — this signals the launcher to provision offline sync support.
|
|
528
|
+
- **Use `'methodName' in host.actions`** to feature-detect new host capabilities rather than comparing `host._version`. The version is informational only.
|
package/package.json
CHANGED
|
@@ -1,556 +0,0 @@
|
|
|
1
|
-
# Scanner Container SDK
|
|
2
|
-
|
|
3
|
-
> **Version:** 1.0 · **Platform:** SmartLinks R4 · **Last updated:** 2026-03-04
|
|
4
|
-
|
|
5
|
-
This document describes how to build a **Scanner Container** — a SmartLinks microapp that replaces the default scanner UI inside the SmartLinks Scanner host application. Scanner containers receive a unified stream of hardware events (RFID, NFC, QR, key presses) and can implement any domain-specific logic on top of raw scan data.
|
|
6
|
-
|
|
7
|
-
> **See also:** [containers.md](containers.md) covers the other container type — portal-embedded full-app containers that run inside web-based SmartLinks portals. Scanner containers are a distinct interface designed specifically for the Android scanner host; they share the UMD bundle format and the `containers` manifest section, but differ in props, build requirements, and runtime context.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Table of Contents
|
|
12
|
-
|
|
13
|
-
1. [Overview](#overview)
|
|
14
|
-
2. [Architecture](#architecture)
|
|
15
|
-
3. [Manifest Declaration](#manifest-declaration)
|
|
16
|
-
4. [Container Props](#container-props)
|
|
17
|
-
5. [Hardware Event Stream](#hardware-event-stream)
|
|
18
|
-
6. [Event Types Reference](#event-types-reference)
|
|
19
|
-
7. [Subscribing to Events](#subscribing-to-events)
|
|
20
|
-
8. [Build & Bundle Requirements](#build--bundle-requirements)
|
|
21
|
-
9. [Shared Dependencies](#shared-dependencies)
|
|
22
|
-
10. [Lifecycle](#lifecycle)
|
|
23
|
-
11. [Example: Minimal Scanner Container](#example-minimal-scanner-container)
|
|
24
|
-
12. [Best Practices](#best-practices)
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Overview
|
|
29
|
-
|
|
30
|
-
The SmartLinks Scanner is a host application that manages hardware readers (RFID, NFC, USB, QR camera) on Android devices. By default it displays scanned tags in a built-in list view with automatic SmartLinks tag resolution.
|
|
31
|
-
|
|
32
|
-
A **Scanner Container** is a microapp that *replaces* this default UI entirely. When a user selects your scanner app, the host:
|
|
33
|
-
|
|
34
|
-
1. Hides its own tag list and lookup logic
|
|
35
|
-
2. Loads your UMD container bundle via `<script>` injection
|
|
36
|
-
3. Renders your exported React component
|
|
37
|
-
4. Forwards **all** hardware events to your component via a pub/sub subscription
|
|
38
|
-
|
|
39
|
-
Your container has full control over how scans are displayed, resolved, and acted upon. The host continues to manage the hardware readers themselves (start/stop RFID, NFC session handling, etc.).
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
┌─────────────────────────────────────────────┐
|
|
43
|
-
│ Scanner Host App │
|
|
44
|
-
│ │
|
|
45
|
-
│ ┌──────────┐ ┌────────────────────────┐ │
|
|
46
|
-
│ │ Hardware │──▶│ Event Dispatcher │ │
|
|
47
|
-
│ │ Bridge │ │ (android-bridge.ts) │ │
|
|
48
|
-
│ └──────────┘ └───────────┬────────────┘ │
|
|
49
|
-
│ │ │
|
|
50
|
-
│ pub/sub │ │
|
|
51
|
-
│ ▼ │
|
|
52
|
-
│ ┌─────────────────────────┐ │
|
|
53
|
-
│ │ Your Scanner │ │
|
|
54
|
-
│ │ Container Component │ │
|
|
55
|
-
│ │ (UMD bundle) │ │
|
|
56
|
-
│ └─────────────────────────┘ │
|
|
57
|
-
└─────────────────────────────────────────────┘
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## Architecture
|
|
63
|
-
|
|
64
|
-
### Host Responsibilities
|
|
65
|
-
|
|
66
|
-
The host application handles:
|
|
67
|
-
|
|
68
|
-
- **Hardware management**: Starting/stopping RFID readers, NFC sessions, QR camera
|
|
69
|
-
- **Key mapping**: Physical trigger buttons (key 293 = scan trigger, key 139 = clear)
|
|
70
|
-
- **Collection context**: User selects a collection before scanning
|
|
71
|
-
- **App discovery**: Fetching widget/container manifests and presenting app selection UI
|
|
72
|
-
- **Bundle loading**: Injecting UMD scripts, resolving exports, managing CSS cleanup
|
|
73
|
-
- **Event forwarding**: Converting raw bridge messages into typed `ScannerEvent` objects
|
|
74
|
-
|
|
75
|
-
### Container Responsibilities
|
|
76
|
-
|
|
77
|
-
Your container handles:
|
|
78
|
-
|
|
79
|
-
- **Scan processing**: Deciding what to do with each event (resolve tags, build UI, etc.)
|
|
80
|
-
- **Data resolution**: Calling SmartLinks APIs to look up tag/product/proof data
|
|
81
|
-
- **Business logic**: Domain-specific workflows (e.g., cask tracking, inventory, quality control)
|
|
82
|
-
- **UI rendering**: Complete control over the scan interface
|
|
83
|
-
|
|
84
|
-
### What the Host Does NOT Do When Your App Is Active
|
|
85
|
-
|
|
86
|
-
When a scanner container is selected, the host **bypasses all local processing**:
|
|
87
|
-
|
|
88
|
-
- No local tag list tracking
|
|
89
|
-
- No automatic SmartLinks tag resolution
|
|
90
|
-
- No deduplication or lookup debouncing
|
|
91
|
-
- Raw events are forwarded directly to your subscriber
|
|
92
|
-
|
|
93
|
-
This ensures zero redundant work and gives your container full ownership of the data flow.
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## Manifest Declaration
|
|
98
|
-
|
|
99
|
-
To be discovered as a scanner container, your app's `app.manifest.json` must declare a container component with `"uiRole": "scanner"`:
|
|
100
|
-
|
|
101
|
-
```json
|
|
102
|
-
{
|
|
103
|
-
"meta": {
|
|
104
|
-
"name": "My Scanner App",
|
|
105
|
-
"appId": "my-scanner-app",
|
|
106
|
-
"version": "1.0.0"
|
|
107
|
-
},
|
|
108
|
-
"containers": {
|
|
109
|
-
"files": {
|
|
110
|
-
"js": { "umd": "containers.umd.js", "esm": "containers.es.js" },
|
|
111
|
-
"css": null
|
|
112
|
-
},
|
|
113
|
-
"components": [
|
|
114
|
-
{
|
|
115
|
-
"name": "ScannerContainer",
|
|
116
|
-
"description": "Custom scanner interface for cask tracking",
|
|
117
|
-
"uiRole": "scanner",
|
|
118
|
-
"scope": "collection",
|
|
119
|
-
"audience": "admin",
|
|
120
|
-
"settings": {
|
|
121
|
-
"type": "object",
|
|
122
|
-
"properties": {
|
|
123
|
-
"autoResolve": {
|
|
124
|
-
"type": "boolean",
|
|
125
|
-
"title": "Auto-resolve tags",
|
|
126
|
-
"description": "Automatically look up tag metadata on scan",
|
|
127
|
-
"default": true
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
]
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### Key Fields
|
|
138
|
-
|
|
139
|
-
| Field | Required | Description |
|
|
140
|
-
| ------------- | -------- | ----------------------------------------------------------------------- |
|
|
141
|
-
| `name` | ✅ | Export name in the UMD bundle (`window.SmartLinksContainers[name]`) |
|
|
142
|
-
| `uiRole` | ✅ | Must be `"scanner"` for the host to recognize it |
|
|
143
|
-
| `description` | ✅ | Shown in the scanner app picker UI |
|
|
144
|
-
| `scope` | Optional | `"collection"` or `"product"` — the data scope |
|
|
145
|
-
| `audience` | Optional | `"admin"`, `"public"`, or `"both"` |
|
|
146
|
-
| `settings` | Optional | JSON Schema describing configurable props (see Widget Settings Schema) |
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
## Container Props
|
|
151
|
-
|
|
152
|
-
The host renders your container with these props:
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
interface ScannerContainerProps {
|
|
156
|
-
/** The active collection ID */
|
|
157
|
-
collectionId: string;
|
|
158
|
-
|
|
159
|
-
/** Your app's unique identifier */
|
|
160
|
-
appId: string;
|
|
161
|
-
|
|
162
|
-
/** Whether the current user is an admin */
|
|
163
|
-
isAdmin: boolean;
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Subscribe to all hardware events.
|
|
167
|
-
* Call once in useEffect; returns an unsubscribe function.
|
|
168
|
-
*/
|
|
169
|
-
onSubscribeScannerEvents: (
|
|
170
|
-
callback: (event: ScannerEvent) => void
|
|
171
|
-
) => (() => void);
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* @deprecated Use onSubscribeScannerEvents instead.
|
|
175
|
-
* Provided for backward compatibility — identical behavior.
|
|
176
|
-
*/
|
|
177
|
-
onSubscribeScanEvents?: (
|
|
178
|
-
callback: (event: ScannerEvent) => void
|
|
179
|
-
) => (() => void);
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
> **Note — no `SL` prop:** Unlike portal containers (see [containers.md](containers.md)), scanner containers do not receive an `SL` prop from the host. Instead, the SmartLinks SDK is externalized to `window.SL` and imported directly in your bundle (`import * as SL from '@proveanything/smartlinks'`). See [Shared Dependencies](#shared-dependencies) for the full externals table.
|
|
184
|
-
|
|
185
|
-
---
|
|
186
|
-
|
|
187
|
-
## Hardware Event Stream
|
|
188
|
-
|
|
189
|
-
All hardware inputs are normalized into a single **discriminated union** type called `ScannerEvent`. The `type` field tells you the source:
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
type ScannerEvent =
|
|
193
|
-
| { type: 'rfid'; uid: string; timestamp: number; rssi?: number }
|
|
194
|
-
| { type: 'nfc'; uid: string; timestamp: number; ndef?: string }
|
|
195
|
-
| { type: 'qr'; data: string; timestamp: number }
|
|
196
|
-
| { type: 'key'; keyCode: number; action: 'down' | 'up'; timestamp: number };
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
### Why a Unified Stream?
|
|
200
|
-
|
|
201
|
-
Rather than providing four separate subscription channels, a single stream:
|
|
202
|
-
|
|
203
|
-
- Simplifies the container API surface (one `useEffect`, one cleanup)
|
|
204
|
-
- Allows containers to correlate cross-device events (e.g., "trigger key held while RFID scans arrive")
|
|
205
|
-
- Makes it trivial to log or replay all hardware activity
|
|
206
|
-
- Avoids race conditions from multiple independent subscriptions
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
|
|
210
|
-
## Event Types Reference
|
|
211
|
-
|
|
212
|
-
### `rfid` — RFID Tag Scan
|
|
213
|
-
|
|
214
|
-
Emitted when the Chainway UHF RFID reader detects a tag.
|
|
215
|
-
|
|
216
|
-
| Field | Type | Description |
|
|
217
|
-
| ----------- | -------- | ----------------------------------------------- |
|
|
218
|
-
| `type` | `'rfid'` | Discriminant |
|
|
219
|
-
| `uid` | `string` | The EPC (Electronic Product Code) hex string |
|
|
220
|
-
| `timestamp` | `number` | `Date.now()` when the event was created |
|
|
221
|
-
| `rssi` | `number` | Signal strength (optional, reader-dependent) |
|
|
222
|
-
|
|
223
|
-
**Notes:**
|
|
224
|
-
- RFID readers emit continuously while active — expect many events per second
|
|
225
|
-
- The same EPC will appear repeatedly; your container should handle deduplication
|
|
226
|
-
- The host starts/stops the RFID reader via hardware key 293 (trigger button)
|
|
227
|
-
|
|
228
|
-
### `nfc` — NFC / USB Tag Scan
|
|
229
|
-
|
|
230
|
-
Emitted when the device's built-in NFC reader or an external USB NFC reader scans a tag.
|
|
231
|
-
|
|
232
|
-
| Field | Type | Description |
|
|
233
|
-
| ----------- | -------- | --------------------------------------------------- |
|
|
234
|
-
| `type` | `'nfc'` | Discriminant |
|
|
235
|
-
| `uid` | `string` | The tag's unique ID (hex string, e.g., `04A3B2...`) |
|
|
236
|
-
| `timestamp` | `number` | `Date.now()` when the event was created |
|
|
237
|
-
| `ndef` | `string` | NDEF record content (URL or text), empty if none |
|
|
238
|
-
|
|
239
|
-
**Notes:**
|
|
240
|
-
- NFC scans are one-shot (tap to scan)
|
|
241
|
-
- Both native NFC and USB reader scans are normalized to this type
|
|
242
|
-
- The `ndef` field often contains a SmartLinks URL that can be parsed for context
|
|
243
|
-
|
|
244
|
-
### `qr` — QR Code Scan
|
|
245
|
-
|
|
246
|
-
Emitted when the native QR code scanner successfully reads a code.
|
|
247
|
-
|
|
248
|
-
| Field | Type | Description |
|
|
249
|
-
| ----------- | ------- | ------------------------------------------ |
|
|
250
|
-
| `type` | `'qr'` | Discriminant |
|
|
251
|
-
| `data` | `string`| The decoded QR code content (URL or text) |
|
|
252
|
-
| `timestamp` | `number`| `Date.now()` when the event was created |
|
|
253
|
-
|
|
254
|
-
**Notes:**
|
|
255
|
-
- Only successful scans are forwarded (cancelled/error scans are filtered)
|
|
256
|
-
- QR scans typically contain SmartLinks URLs or serial numbers
|
|
257
|
-
|
|
258
|
-
### `key` — Hardware Key Press
|
|
259
|
-
|
|
260
|
-
Emitted when a physical button is pressed or released on the device.
|
|
261
|
-
|
|
262
|
-
| Field | Type | Description |
|
|
263
|
-
| ----------- | ----------------- | ------------------------------------------- |
|
|
264
|
-
| `type` | `'key'` | Discriminant |
|
|
265
|
-
| `keyCode` | `number` | Android key code constant |
|
|
266
|
-
| `action` | `'down' \| 'up'` | Press or release |
|
|
267
|
-
| `timestamp` | `number` | `Date.now()` when the event was created |
|
|
268
|
-
|
|
269
|
-
**Common Key Codes:**
|
|
270
|
-
|
|
271
|
-
| Code | Button | Default Host Behavior |
|
|
272
|
-
| ----- | --------------- | -------------------------------------------- |
|
|
273
|
-
| `293` | Scan trigger | Start RFID on DOWN, stop on UP |
|
|
274
|
-
| `139` | Function/Clear | Clear tag list on DOWN |
|
|
275
|
-
|
|
276
|
-
**Notes:**
|
|
277
|
-
- Key events are forwarded to your container **and** processed by the host simultaneously
|
|
278
|
-
- The host will still start/stop RFID reading on key 293 — your container receives the resulting RFID events
|
|
279
|
-
- You can use key events for custom actions (e.g., confirm selection, switch modes)
|
|
280
|
-
|
|
281
|
-
---
|
|
282
|
-
|
|
283
|
-
## Subscribing to Events
|
|
284
|
-
|
|
285
|
-
Use the `onSubscribeScannerEvents` prop in a `useEffect`:
|
|
286
|
-
|
|
287
|
-
```tsx
|
|
288
|
-
import { useEffect } from 'react';
|
|
289
|
-
|
|
290
|
-
export function ScannerContainer({ onSubscribeScannerEvents, collectionId, appId }) {
|
|
291
|
-
const [scans, setScans] = useState([]);
|
|
292
|
-
|
|
293
|
-
useEffect(() => {
|
|
294
|
-
const unsubscribe = onSubscribeScannerEvents((event) => {
|
|
295
|
-
switch (event.type) {
|
|
296
|
-
case 'rfid':
|
|
297
|
-
console.log('RFID:', event.uid, 'RSSI:', event.rssi);
|
|
298
|
-
// Deduplicate + resolve against SmartLinks
|
|
299
|
-
break;
|
|
300
|
-
case 'nfc':
|
|
301
|
-
console.log('NFC:', event.uid, 'NDEF:', event.ndef);
|
|
302
|
-
break;
|
|
303
|
-
case 'qr':
|
|
304
|
-
console.log('QR:', event.data);
|
|
305
|
-
break;
|
|
306
|
-
case 'key':
|
|
307
|
-
console.log('Key:', event.keyCode, event.action);
|
|
308
|
-
break;
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
return unsubscribe; // Clean up on unmount
|
|
313
|
-
}, [onSubscribeScannerEvents]);
|
|
314
|
-
|
|
315
|
-
return <div>...</div>;
|
|
316
|
-
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
### Important Patterns
|
|
320
|
-
|
|
321
|
-
1. **Subscribe once** — The subscriber function is stable (wrapped in `useCallback` by the host). Subscribe in a `useEffect` with `[onSubscribeScannerEvents]` as the dependency.
|
|
322
|
-
|
|
323
|
-
2. **Use refs for mutable state** — If your event handler needs access to current state, use refs to avoid stale closures:
|
|
324
|
-
|
|
325
|
-
```tsx
|
|
326
|
-
const scansRef = useRef(new Map());
|
|
327
|
-
|
|
328
|
-
useEffect(() => {
|
|
329
|
-
const unsubscribe = onSubscribeScannerEvents((event) => {
|
|
330
|
-
if (event.type === 'rfid') {
|
|
331
|
-
scansRef.current.set(event.uid, event);
|
|
332
|
-
// Trigger re-render via setState
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
return unsubscribe;
|
|
336
|
-
}, [onSubscribeScannerEvents]);
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
3. **Batch state updates** — RFID events can arrive at high frequency. Consider debouncing UI updates while accumulating events.
|
|
340
|
-
|
|
341
|
-
---
|
|
342
|
-
|
|
343
|
-
## Build & Bundle Requirements
|
|
344
|
-
|
|
345
|
-
Scanner containers must be built as **UMD bundles** that register exports on `window.SmartLinksContainers`:
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
// vite.config.container.ts
|
|
349
|
-
export default defineConfig({
|
|
350
|
-
build: {
|
|
351
|
-
lib: {
|
|
352
|
-
entry: 'src/containers/index.ts',
|
|
353
|
-
name: 'SmartLinksContainers', // ← window global name
|
|
354
|
-
formats: ['umd'],
|
|
355
|
-
fileName: 'containers',
|
|
356
|
-
},
|
|
357
|
-
rollupOptions: {
|
|
358
|
-
external: [/* shared dependencies — see below */],
|
|
359
|
-
output: {
|
|
360
|
-
globals: {/* dependency → window global mapping */},
|
|
361
|
-
},
|
|
362
|
-
},
|
|
363
|
-
},
|
|
364
|
-
});
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
### Export Structure
|
|
368
|
-
|
|
369
|
-
```typescript
|
|
370
|
-
// src/containers/index.ts
|
|
371
|
-
export { ScannerContainer } from './ScannerContainer';
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
The host resolves your component by the `name` field in the manifest:
|
|
375
|
-
|
|
376
|
-
```javascript
|
|
377
|
-
const Component = window.SmartLinksContainers['ScannerContainer'];
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
---
|
|
381
|
-
|
|
382
|
-
## Shared Dependencies
|
|
383
|
-
|
|
384
|
-
The host exposes these libraries on `window` — your container build **must externalize them** (do not bundle):
|
|
385
|
-
|
|
386
|
-
| Import | Window Global |
|
|
387
|
-
| ----------------------------- | ----------------------- |
|
|
388
|
-
| `react` | `window.React` |
|
|
389
|
-
| `react-dom` | `window.ReactDOM` |
|
|
390
|
-
| `react/jsx-runtime` | `window.jsxRuntime` |
|
|
391
|
-
| `@proveanything/smartlinks` | `window.SL` |
|
|
392
|
-
| `class-variance-authority` | `window.CVA` |
|
|
393
|
-
| `react-router-dom` | `window.ReactRouterDOM` |
|
|
394
|
-
| `@tanstack/react-query` | `window.ReactQuery` |
|
|
395
|
-
| `lucide-react` | `window.LucideReact` |
|
|
396
|
-
| `date-fns` | `window.dateFns` |
|
|
397
|
-
| `@radix-ui/react-*` | `window.Radix*` |
|
|
398
|
-
|
|
399
|
-
See the [SmartLinks Microapp Development Guide](../README.md) for the full shared dependencies table with exact global names.
|
|
400
|
-
|
|
401
|
-
Any dependency **not** in this list must be bundled into your container.
|
|
402
|
-
|
|
403
|
-
---
|
|
404
|
-
|
|
405
|
-
## Lifecycle
|
|
406
|
-
|
|
407
|
-
```
|
|
408
|
-
1. User selects collection
|
|
409
|
-
2. Host fetches widget/container manifests via SmartLinks API
|
|
410
|
-
3. Host identifies containers with uiRole === 'scanner'
|
|
411
|
-
4. User picks your scanner app from the list
|
|
412
|
-
→ Selection is persisted in localStorage per collection
|
|
413
|
-
5. Host loads your UMD bundle via <script> tag
|
|
414
|
-
6. Host resolves your component from window.SmartLinksContainers
|
|
415
|
-
7. Host renders <YourComponent ...props />
|
|
416
|
-
8. Your component subscribes to onSubscribeScannerEvents
|
|
417
|
-
9. User presses hardware trigger → RFID/NFC/QR events flow to your callback
|
|
418
|
-
10. User switches away or deselects → component unmounts, script/CSS removed
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
### Cleanup
|
|
422
|
-
|
|
423
|
-
The host handles all cleanup when your container is deselected or the collection changes:
|
|
424
|
-
|
|
425
|
-
- Script tag removal
|
|
426
|
-
- Injected CSS/style removal
|
|
427
|
-
- React component unmounting
|
|
428
|
-
- Event listener cleanup (via your returned unsubscribe function)
|
|
429
|
-
|
|
430
|
-
---
|
|
431
|
-
|
|
432
|
-
## Example: Minimal Scanner Container
|
|
433
|
-
|
|
434
|
-
```tsx
|
|
435
|
-
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
436
|
-
import * as SL from '@proveanything/smartlinks';
|
|
437
|
-
|
|
438
|
-
// Types — import from your own types file or declare inline
|
|
439
|
-
type ScannerEvent =
|
|
440
|
-
| { type: 'rfid'; uid: string; timestamp: number; rssi?: number }
|
|
441
|
-
| { type: 'nfc'; uid: string; timestamp: number; ndef?: string }
|
|
442
|
-
| { type: 'qr'; data: string; timestamp: number }
|
|
443
|
-
| { type: 'key'; keyCode: number; action: 'down' | 'up'; timestamp: number };
|
|
444
|
-
|
|
445
|
-
interface Props {
|
|
446
|
-
collectionId: string;
|
|
447
|
-
appId: string;
|
|
448
|
-
isAdmin: boolean;
|
|
449
|
-
onSubscribeScannerEvents: (cb: (event: ScannerEvent) => void) => () => void;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
interface ScannedTag {
|
|
453
|
-
uid: string;
|
|
454
|
-
source: 'rfid' | 'nfc' | 'qr';
|
|
455
|
-
firstSeen: number;
|
|
456
|
-
count: number;
|
|
457
|
-
productName?: string;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
export function ScannerContainer({ collectionId, appId, isAdmin, onSubscribeScannerEvents }: Props) {
|
|
461
|
-
const [tags, setTags] = useState<Map<string, ScannedTag>>(new Map());
|
|
462
|
-
const tagsRef = useRef(tags);
|
|
463
|
-
tagsRef.current = tags;
|
|
464
|
-
|
|
465
|
-
const resolveTag = useCallback(async (cId: string, uid: string) => {
|
|
466
|
-
try {
|
|
467
|
-
const result = await SL.tags.publicGetByCollection(cId, uid, 'product');
|
|
468
|
-
const product = result.embedded?.products?.[result.tag?.productId!];
|
|
469
|
-
setTags(prev => {
|
|
470
|
-
const next = new Map(prev);
|
|
471
|
-
const tag = next.get(uid);
|
|
472
|
-
if (tag) {
|
|
473
|
-
next.set(uid, { ...tag, productName: product?.name ?? 'Unknown' });
|
|
474
|
-
}
|
|
475
|
-
return next;
|
|
476
|
-
});
|
|
477
|
-
} catch (err) {
|
|
478
|
-
console.error('Tag resolution failed:', uid, err);
|
|
479
|
-
}
|
|
480
|
-
}, []);
|
|
481
|
-
|
|
482
|
-
useEffect(() => {
|
|
483
|
-
const unsubscribe = onSubscribeScannerEvents((event) => {
|
|
484
|
-
if (event.type === 'rfid' || event.type === 'nfc') {
|
|
485
|
-
const uid = event.uid;
|
|
486
|
-
setTags(prev => {
|
|
487
|
-
const next = new Map(prev);
|
|
488
|
-
const existing = next.get(uid);
|
|
489
|
-
if (existing) {
|
|
490
|
-
next.set(uid, { ...existing, count: existing.count + 1 });
|
|
491
|
-
} else {
|
|
492
|
-
next.set(uid, {
|
|
493
|
-
uid,
|
|
494
|
-
source: event.type,
|
|
495
|
-
firstSeen: event.timestamp,
|
|
496
|
-
count: 1,
|
|
497
|
-
});
|
|
498
|
-
// Resolve tag in background
|
|
499
|
-
resolveTag(collectionId, uid);
|
|
500
|
-
}
|
|
501
|
-
return next;
|
|
502
|
-
});
|
|
503
|
-
} else if (event.type === 'qr') {
|
|
504
|
-
console.log('QR scanned:', event.data);
|
|
505
|
-
}
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
return unsubscribe;
|
|
509
|
-
}, [onSubscribeScannerEvents, collectionId, resolveTag]);
|
|
510
|
-
|
|
511
|
-
return (
|
|
512
|
-
<div style={{ padding: 16 }}>
|
|
513
|
-
<h2>Scanned Tags ({tags.size})</h2>
|
|
514
|
-
{Array.from(tags.values()).map(tag => (
|
|
515
|
-
<div key={tag.uid} style={{ padding: 8, borderBottom: '1px solid #eee' }}>
|
|
516
|
-
<strong>{tag.uid}</strong> × {tag.count}
|
|
517
|
-
{tag.productName && <span> — {tag.productName}</span>}
|
|
518
|
-
<span style={{ opacity: 0.5, marginLeft: 8 }}>{tag.source.toUpperCase()}</span>
|
|
519
|
-
</div>
|
|
520
|
-
))}
|
|
521
|
-
{tags.size === 0 && <p style={{ opacity: 0.5 }}>Waiting for scans...</p>}
|
|
522
|
-
</div>
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
---
|
|
528
|
-
|
|
529
|
-
## Best Practices
|
|
530
|
-
|
|
531
|
-
### Performance
|
|
532
|
-
|
|
533
|
-
- **Debounce UI updates** for RFID — you may receive 50+ events/second during active scanning
|
|
534
|
-
- **Use refs** for state accessed inside the event callback to avoid stale closures
|
|
535
|
-
- **Batch API calls** — use `SL.tags.lookupTags()` for batch resolution rather than one call per tag
|
|
536
|
-
|
|
537
|
-
### Data Resolution
|
|
538
|
-
|
|
539
|
-
- **Prefer collection-scoped lookups** — since you receive `collectionId` as a prop, always use `SL.tags.publicGetByCollection(collectionId, tagId)` for fast, direct resolution
|
|
540
|
-
- **Use `SL.tags.resolveTag` only as fallback** — for the rare case where a tag might belong to a different collection
|
|
541
|
-
|
|
542
|
-
### UX
|
|
543
|
-
|
|
544
|
-
- **Show scan feedback immediately** — display the raw UID before resolution completes
|
|
545
|
-
- **Handle key events thoughtfully** — the host already manages RFID start/stop via key 293; use key events for your own UI actions (confirm, navigate, toggle modes)
|
|
546
|
-
- **Support high-density scanning** — RFID use cases often involve scanning 50–100+ tags in a session
|
|
547
|
-
|
|
548
|
-
### Error Handling
|
|
549
|
-
|
|
550
|
-
- **Gracefully handle missing tags** — not every scanned EPC/UID will resolve to a SmartLinks tag
|
|
551
|
-
- **Network resilience** — tag resolution may fail; show cached/partial data and retry
|
|
552
|
-
|
|
553
|
-
### Bundle Size
|
|
554
|
-
|
|
555
|
-
- **Externalize shared dependencies** — never bundle React, SmartLinks SDK, or other shared libs
|
|
556
|
-
- **Keep containers focused** — a scanner container should do one thing well
|