@proveanything/smartlinks 1.7.2 → 1.7.4
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/api/comms.d.ts +1 -1
- package/dist/api/comms.js +2 -2
- package/dist/api/proof.d.ts +38 -0
- package/dist/api/proof.js +40 -0
- package/dist/docs/API_SUMMARY.md +10 -3
- package/dist/docs/ai.md +289 -0
- package/dist/docs/comms.md +1 -1
- package/dist/docs/scanner-container.md +556 -0
- package/dist/http.js +30 -1
- package/dist/openapi.yaml +77 -33
- package/dist/types/comms.d.ts +1 -1
- package/docs/API_SUMMARY.md +10 -3
- package/docs/ai.md +289 -0
- package/docs/comms.md +1 -1
- package/docs/scanner-container.md +556 -0
- package/openapi.yaml +77 -33
- package/package.json +1 -1
|
@@ -0,0 +1,556 @@
|
|
|
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
|
package/openapi.yaml
CHANGED
|
@@ -2138,6 +2138,39 @@ paths:
|
|
|
2138
2138
|
description: Unauthorized
|
|
2139
2139
|
404:
|
|
2140
2140
|
description: Not found
|
|
2141
|
+
/admin/collection/{collectionId}/comm.send:
|
|
2142
|
+
post:
|
|
2143
|
+
tags:
|
|
2144
|
+
- comms
|
|
2145
|
+
summary: Send a single transactional message to one contact using a template.
|
|
2146
|
+
operationId: comms_sendTransactional
|
|
2147
|
+
security:
|
|
2148
|
+
- bearerAuth: []
|
|
2149
|
+
parameters:
|
|
2150
|
+
- name: collectionId
|
|
2151
|
+
in: path
|
|
2152
|
+
required: true
|
|
2153
|
+
schema:
|
|
2154
|
+
type: string
|
|
2155
|
+
responses:
|
|
2156
|
+
200:
|
|
2157
|
+
description: Success
|
|
2158
|
+
content:
|
|
2159
|
+
application/json:
|
|
2160
|
+
schema:
|
|
2161
|
+
$ref: "#/components/schemas/TransactionalSendResult"
|
|
2162
|
+
400:
|
|
2163
|
+
description: Bad request
|
|
2164
|
+
401:
|
|
2165
|
+
description: Unauthorized
|
|
2166
|
+
404:
|
|
2167
|
+
description: Not found
|
|
2168
|
+
requestBody:
|
|
2169
|
+
required: true
|
|
2170
|
+
content:
|
|
2171
|
+
application/json:
|
|
2172
|
+
schema:
|
|
2173
|
+
$ref: "#/components/schemas/TransactionalSendRequest"
|
|
2141
2174
|
/admin/collection/{collectionId}/comm.settings:
|
|
2142
2175
|
patch:
|
|
2143
2176
|
tags:
|
|
@@ -2421,39 +2454,6 @@ paths:
|
|
|
2421
2454
|
application/json:
|
|
2422
2455
|
schema:
|
|
2423
2456
|
$ref: "#/components/schemas/CommsRecipientsWithoutActionQuery"
|
|
2424
|
-
/admin/collection/{collectionId}/comm/send:
|
|
2425
|
-
post:
|
|
2426
|
-
tags:
|
|
2427
|
-
- comms
|
|
2428
|
-
summary: Send a single transactional message to one contact using a template.
|
|
2429
|
-
operationId: comms_sendTransactional
|
|
2430
|
-
security:
|
|
2431
|
-
- bearerAuth: []
|
|
2432
|
-
parameters:
|
|
2433
|
-
- name: collectionId
|
|
2434
|
-
in: path
|
|
2435
|
-
required: true
|
|
2436
|
-
schema:
|
|
2437
|
-
type: string
|
|
2438
|
-
responses:
|
|
2439
|
-
200:
|
|
2440
|
-
description: Success
|
|
2441
|
-
content:
|
|
2442
|
-
application/json:
|
|
2443
|
-
schema:
|
|
2444
|
-
$ref: "#/components/schemas/TransactionalSendResult"
|
|
2445
|
-
400:
|
|
2446
|
-
description: Bad request
|
|
2447
|
-
401:
|
|
2448
|
-
description: Unauthorized
|
|
2449
|
-
404:
|
|
2450
|
-
description: Not found
|
|
2451
|
-
requestBody:
|
|
2452
|
-
required: true
|
|
2453
|
-
content:
|
|
2454
|
-
application/json:
|
|
2455
|
-
schema:
|
|
2456
|
-
$ref: "#/components/schemas/TransactionalSendRequest"
|
|
2457
2457
|
/admin/collection/{collectionId}/contacts:
|
|
2458
2458
|
get:
|
|
2459
2459
|
tags:
|
|
@@ -5203,6 +5203,50 @@ paths:
|
|
|
5203
5203
|
description: Unauthorized
|
|
5204
5204
|
404:
|
|
5205
5205
|
description: Not found
|
|
5206
|
+
/admin/collection/{collectionId}/products/{productId}/proofs/{proofId}/migrate:
|
|
5207
|
+
post:
|
|
5208
|
+
tags:
|
|
5209
|
+
- proof
|
|
5210
|
+
summary: proof.migrate
|
|
5211
|
+
operationId: proof_migrate
|
|
5212
|
+
security:
|
|
5213
|
+
- bearerAuth: []
|
|
5214
|
+
parameters:
|
|
5215
|
+
- name: collectionId
|
|
5216
|
+
in: path
|
|
5217
|
+
required: true
|
|
5218
|
+
schema:
|
|
5219
|
+
type: string
|
|
5220
|
+
- name: productId
|
|
5221
|
+
in: path
|
|
5222
|
+
required: true
|
|
5223
|
+
schema:
|
|
5224
|
+
type: string
|
|
5225
|
+
- name: proofId
|
|
5226
|
+
in: path
|
|
5227
|
+
required: true
|
|
5228
|
+
schema:
|
|
5229
|
+
type: string
|
|
5230
|
+
responses:
|
|
5231
|
+
200:
|
|
5232
|
+
description: Success
|
|
5233
|
+
content:
|
|
5234
|
+
application/json:
|
|
5235
|
+
schema:
|
|
5236
|
+
$ref: "#/components/schemas/ProofResponse"
|
|
5237
|
+
400:
|
|
5238
|
+
description: Bad request
|
|
5239
|
+
401:
|
|
5240
|
+
description: Unauthorized
|
|
5241
|
+
404:
|
|
5242
|
+
description: Not found
|
|
5243
|
+
requestBody:
|
|
5244
|
+
required: true
|
|
5245
|
+
content:
|
|
5246
|
+
application/json:
|
|
5247
|
+
schema:
|
|
5248
|
+
type: object
|
|
5249
|
+
additionalProperties: true
|
|
5206
5250
|
/admin/collection/{collectionId}/proof/findByUser/{userId}:
|
|
5207
5251
|
get:
|
|
5208
5252
|
tags:
|