@proveanything/smartlinks 2.0.43 → 2.0.45

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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.43 | Generated: 2026-10-06T15:37:31.849Z
3
+ Version: 2.0.45 | Generated: 2026-10-06T17:22:34.344Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -86,11 +86,11 @@ interface AdminMobileHostContext {
86
86
  // Unified hardware event stream — callback type: AdminMobileEventCallback
87
87
  events: { subscribe: (cb: AdminMobileEventCallback) => () => void }
88
88
 
89
- // Promise-based hardware actions — reject with a structured error when unavailable
89
+ // Promise-based hardware actions — reject with a structured error when unavailable or cancelled
90
90
  actions: {
91
- requestQrScan: () => Promise<string>
92
- requestNfcTap: (timeoutMs?: number) => Promise<{ uid: string; ndef?: string }>
93
- requestCameraPhoto: () => Promise<Blob>
91
+ requestQrScan: (opts?: { signal?: AbortSignal; heightPercent?: number; prompt?: string }) => Promise<string>
92
+ requestNfcTap: (opts?: { timeoutMs?: number; signal?: AbortSignal; prompt?: string } | number) => Promise<{ uid: string; ndef?: string }>
93
+ requestCameraPhoto: (opts?: { signal?: AbortSignal }) => Promise<Blob>
94
94
  share: (payload: { title: string; url: string; text?: string }) => Promise<void>
95
95
  clipboard: {
96
96
  read: () => Promise<string>
@@ -106,6 +106,9 @@ interface AdminMobileHostContext {
106
106
  navigateBack?: () => void
107
107
  }
108
108
 
109
+ // UHF RFID reader — only on hosts with one (feature-detect). See "RFID" below.
110
+ rfid?: AdminMobileRfid
111
+
109
112
  // Network & device info
110
113
  network: { isOnline: () => boolean }
111
114
  device: { info: () => Promise<{ model: string; platform: string }> }
@@ -331,6 +334,92 @@ The SmartLinks Mobile launcher loads your `mobileAdmin` bundle through the platf
331
334
 
332
335
  ---
333
336
 
337
+ ## Scanning: listening vs asking
338
+
339
+ Every scan reaches a container in one of two ways. Pick per screen.
340
+
341
+ | | Listening — `host.events` | Asking — `host.actions.request*` |
342
+ |---|---|---|
343
+ | Use for | "Tap or scan anything, any time" — a stock take, a scan-to-open list | "This step needs a scan" — tag this item, scan the shelf code |
344
+ | What the user sees | Nothing changes | NFC: a small non-blocking hint with Cancel. QR / photo: the host's camera opens |
345
+ | What you get | An event per tap / read / key | A promise for one result |
346
+
347
+ ### NFC: two modes
348
+
349
+ - **Listening** — subscribe to `nfc-tap`. Fires for every tap while no request is waiting.
350
+ - **Asking** — `await host.actions.requestNfcTap({ prompt: 'Tap the bottle tag', signal })`. The next tap
351
+ **made after the call** resolves it, and that tap is **not** also sent as an `nfc-tap` event, so
352
+ nothing handles it twice. The request is non-blocking: the screen stays usable while it waits — show
353
+ your own "or type the code" path and abort the wait with `signal` if the user takes it. One request at
354
+ a time: a new call cancels the previous one.
355
+
356
+ ```typescript
357
+ const controller = new AbortController()
358
+ try {
359
+ const { uid } = await host.actions.requestNfcTap({ prompt: 'Tap the bottle tag', signal: controller.signal })
360
+ await attachTag(uid)
361
+ } catch (err) {
362
+ if (err instanceof HostCancelledError) return // they pressed Cancel, or we aborted
363
+ throw err
364
+ }
365
+ // elsewhere — the user typed the code instead:
366
+ controller.abort()
367
+ ```
368
+
369
+ ### QR and photos
370
+
371
+ `requestQrScan()` opens the host's camera scanner (full screen, or the bottom `heightPercent` of it) and
372
+ resolves with the code; `requestCameraPhoto()` opens the camera and resolves with the image. Both reject
373
+ with `HostCancelledError` when the user backs out.
374
+
375
+ ### RFID
376
+
377
+ `host.rfid` gives a container the same reader abilities the host's own scanner uses — so an app can be
378
+ its own scanner. It's present only when the device has a UHF reader (`hardware.rfid`); check for it.
379
+ There's one reader, so starting any of the three below ends the one before, and everything stops when
380
+ the container unmounts.
381
+
382
+ **Keep reading** — a stock take, an intake. De-duplicated and batched, so you get each tag once instead
383
+ of hundreds of raw reads a second:
384
+
385
+ ```typescript
386
+ const session = host.rfid?.read({
387
+ filter: { prefix: '3034' }, // optional: only EPCs starting with this (or { epcs: [...] })
388
+ dedupe: 'session', // default — each tag once; { windowMs: 5000 } or 'none' for every read
389
+ batchMs: 250, // default — reads arrive in batches; 0 = one at a time
390
+ mode: 'trigger', // default — reads while the operator holds the trigger; 'continuous' = now until stop()
391
+ })
392
+ const off = session?.onRead((reads) => { // [{ epc, tid?, rssi?, timestamp, count }]
393
+ for (const r of reads) addToCount(r.epc)
394
+ })
395
+ // later
396
+ session?.stop()
397
+ ```
398
+
399
+ **One tag for this step** — tag an item. Resolves with the strongest tag read in the first moment of
400
+ reading, then stops; the RFID counterpart of `requestNfcTap` (non-blocking, abortable, `prompt` hint):
401
+
402
+ ```typescript
403
+ const { epc } = await host.rfid.readOne({ prompt: 'Pull the trigger over the item', signal })
404
+ ```
405
+
406
+ **Find a tag** — a Geiger counter. `strength` runs 0 (not in range) to 100 (right here); by default the
407
+ host plays faster beeps and haptics as you close in (`feedback: false` to do your own):
408
+
409
+ ```typescript
410
+ const hunt = host.rfid?.locate(item.epcs) // one EPC, or every tag on the item
411
+ hunt?.onUpdate(({ strength, rssi, hitsPerSecond }) => setStrength(strength))
412
+ // hunt.stop() when found
413
+ ```
414
+
415
+ `status()` / `onStatus(cb)` report whether a reader is available and connected, which one (`built-in`,
416
+ `zebra`, `tsl`), whether it's reading, and its power range; `read`/`locate` take a `power` level for the
417
+ session.
418
+
419
+ Raw reads still arrive as `rfid-burst` events whenever the reader runs, for containers that want them.
420
+
421
+ ---
422
+
334
423
  ## Event Stream
335
424
 
336
425
  For raw scan events (rather than the action-driven `host.actions.requestNfcTap`), subscribe to the unified event stream. Events flow identically on every host — the launcher normalises bridge messages, Capacitor callbacks, and Web API events into the same shape.
@@ -350,6 +439,13 @@ useEffect(() => {
350
439
  }, [host.events])
351
440
  ```
352
441
 
442
+ Event details:
443
+
444
+ - Every event may carry `timestamp` (ms since epoch).
445
+ - `nfc-tap` — `uid` is uppercase hex without separators (`04A1B2C3D4E5F6`); `ndef` is the tag's NDEF text (usually its URL), `""` when it had none.
446
+ - `rfid-burst` — reads arrive as they happen, typically **one EPC per event, many per second** while the trigger is held. Collect and de-duplicate them yourself. `rssi` (dBm; closer = higher) when the reader reports it.
447
+ - `key-press` — hosts that report both edges send `action: 'down'` then `'up'` for one press; act on `'down'` (or on events without `action`). `293` is the scan trigger.
448
+
353
449
  Lifecycle events (`event.type === 'lifecycle'`) carry a `phase` field: `'pause' | 'resume' | 'offline' | 'online'`. Use these instead of `window` event listeners so your container works identically on all hosts.
354
450
 
355
451
  ---
@@ -372,8 +468,15 @@ class HostTimeoutError extends Error {
372
468
  capability: 'nfc' | 'qr'
373
469
  timeoutMs: number
374
470
  }
471
+
472
+ class HostCancelledError extends Error {
473
+ capability: 'nfc' | 'qr' | 'camera'
474
+ by: 'user' | 'app' // closed the scanner / pressed Cancel, or aborted by the container
475
+ }
375
476
  ```
376
477
 
478
+ All four are exported from `@proveanything/smartlinks`; check with `instanceof` or `err.name`.
479
+
377
480
  ```typescript
378
481
  try {
379
482
  const code = await host.actions.requestQrScan()
package/dist/index.d.ts CHANGED
@@ -31,9 +31,9 @@ export type { Collection, CollectionResponse, CollectionCreateRequest, Collectio
31
31
  export type { Proof, ProofResponse, ProofWrite, ProofCreateRequest, ProofUpdateRequest, ProofClaimRequest, ProofGrant, GrantScope, GrantAudience, CreateGrantOptions, RedeemGrantOptions, RedeemGrantResult, ProofTransfer, ProofTransferType, ProofTransferState, TransferProofOptions, TransferProofResult, } from "./types/proof.js";
32
32
  export type { QrShortCodeLookupResponse, } from "./types/qr.js";
33
33
  export type { ReverseTagLookupParams, ReverseTagLookupResponse, } from "./types/tags.js";
34
- export type { AdminMobileCapability, ActionableCapability, AdminMobileHostId, AdminMobileEvent, AdminMobileEventCallback, AdminMobileEventSubscriber, ScannerEventSubscriber, // @deprecated — use AdminMobileEventCallback
34
+ export type { AdminMobileCapability, ActionableCapability, AdminMobileHostId, AdminMobileEvent, AdminMobileEventCallback, AdminMobileEventSubscriber, NfcTapRequestOptions, QrScanRequestOptions, CameraPhotoRequestOptions, AdminMobileRfid, RfidRead, RfidReaderStatus, RfidFilter, RfidReadOptions, RfidReadSession, RfidReadOneOptions, RfidLocateOptions, RfidLocateUpdate, RfidLocateSession, ScannerEventSubscriber, // @deprecated — use AdminMobileEventCallback
35
35
  AdminMobileHostContext, AdminMobileComponentManifest, AdminMobileBundleManifest, MobileAdminComponentManifest, // @deprecated — use AdminMobileComponentManifest
36
36
  MobileAdminBundleManifest, } from './mobile-admin/types.js';
37
- export { HostCapabilityUnavailableError, HostPermissionDeniedError, HostTimeoutError, } from './mobile-admin/errors.js';
37
+ export { HostCapabilityUnavailableError, HostPermissionDeniedError, HostTimeoutError, HostCancelledError, } from './mobile-admin/errors.js';
38
38
  export type { NativeCapability, NativeFacade, ShareFacade, ClipboardFacade, HapticImpactStyle, HapticNotificationStyle, HapticsFacade, NetworkStatus, NetworkFacade, DeviceInfo, DeviceFacade, StorageFacade, QrScanOptions, QrFacade, AuthFacade, NfcReadResult, NfcFacade, RfidScanOptions, RfidFacade, EventsFacade, WebSourceMode, WebSourceConfig, WebSourceFacade, } from './native/types.js';
39
39
  export type { AuthKitUser, UserProfile, ProfileUpdateData, UpdateProfileResponse, SuccessResponse, AuthLoginResponse, AppleLoginOptions, AuthKitErrorCode, RefreshResponse, LogoutResponse, RefreshErrorCode, MagicLinkSendResponse, MagicLinkVerifyResponse, PhoneSendCodeResponse, PhoneVerifyResponse, PasswordResetRequestResponse, VerifyResetTokenResponse, PasswordResetCompleteResponse, EmailVerificationActionResponse, EmailVerifyTokenResponse, VerifyStatus, WhatsAppReplyCta, WhatsAppReplyOptions, WhatsAppContactData, SendWhatsAppRequest, SendWhatsAppResponse, ExchangeWhatsAppSessionResponse, VerifyWhatsAppResponse, WhatsAppStatusResponse, SendSmsVerifyRequest, SendSmsVerifyResponse, VerifySmsResponse, UpsertContactRequest, UpsertContactResponse, AuthKitBrandingConfig, AuthKitConfig, AuthKitConfigInput, AuthKitSecurityConfig, AuthKitPasswordPolicy, AuthKitSessionPolicy, AuthKitLockoutPolicy, PasswordPolicyErrorCode, LoginSecurityErrorCode, } from './types/authKit.js';
package/dist/index.js CHANGED
@@ -23,4 +23,4 @@ export { headless_1 as headless };
23
23
  export { SHARED_DEPENDENCY_CONTRACT_VERSION, SHARED_DEPENDENCIES, SHARED_DEPENDENCY_SPECIFIERS, importMapPathFor, getHostSharedDependencies, } from './shared-dependencies.js';
24
24
  // Built-in AI tool catalog (design-time discovery of the core agentic toolset)
25
25
  export { AI_TOOL_NAMES, BUILTIN_AI_TOOLS, getBuiltinAiTool } from './ai-tools.js';
26
- export { HostCapabilityUnavailableError, HostPermissionDeniedError, HostTimeoutError, } from './mobile-admin/errors.js';
26
+ export { HostCapabilityUnavailableError, HostPermissionDeniedError, HostTimeoutError, HostCancelledError, } from './mobile-admin/errors.js';
@@ -59,8 +59,29 @@ export declare class HostPermissionDeniedError extends Error {
59
59
  */
60
60
  export declare class HostTimeoutError extends Error {
61
61
  /** The capability that timed out. */
62
- capability: Extract<ActionableCapability, 'nfc' | 'qr' | 'geolocation'>;
62
+ capability: Extract<ActionableCapability, 'nfc' | 'qr' | 'rfid' | 'geolocation'>;
63
63
  /** The timeout threshold in milliseconds. */
64
64
  timeoutMs: number;
65
65
  constructor(capability: HostTimeoutError['capability'], timeoutMs: number);
66
66
  }
67
+ /**
68
+ * Thrown when a host action ends without a result because someone stopped it: the user closed the
69
+ * scanner / pressed Cancel on the "tap a tag" hint (`by: 'user'`), or the container aborted it with its
70
+ * `signal` or replaced it with a newer request (`by: 'app'`). Usually not an error to show — the user
71
+ * chose to stop.
72
+ *
73
+ * @example
74
+ * try {
75
+ * const code = await host.actions.requestQrScan();
76
+ * } catch (err) {
77
+ * if (err instanceof HostCancelledError) return; // they closed the scanner
78
+ * throw err;
79
+ * }
80
+ */
81
+ export declare class HostCancelledError extends Error {
82
+ /** The capability whose action was cancelled. */
83
+ capability: ActionableCapability;
84
+ /** Who stopped it. */
85
+ by: 'user' | 'app';
86
+ constructor(capability: ActionableCapability, by?: 'user' | 'app');
87
+ }
@@ -70,3 +70,26 @@ export class HostTimeoutError extends Error {
70
70
  Object.setPrototypeOf(this, new.target.prototype);
71
71
  }
72
72
  }
73
+ /**
74
+ * Thrown when a host action ends without a result because someone stopped it: the user closed the
75
+ * scanner / pressed Cancel on the "tap a tag" hint (`by: 'user'`), or the container aborted it with its
76
+ * `signal` or replaced it with a newer request (`by: 'app'`). Usually not an error to show — the user
77
+ * chose to stop.
78
+ *
79
+ * @example
80
+ * try {
81
+ * const code = await host.actions.requestQrScan();
82
+ * } catch (err) {
83
+ * if (err instanceof HostCancelledError) return; // they closed the scanner
84
+ * throw err;
85
+ * }
86
+ */
87
+ export class HostCancelledError extends Error {
88
+ constructor(capability, by = 'user') {
89
+ super(`Capability '${capability}' was cancelled by the ${by}`);
90
+ this.name = 'HostCancelledError';
91
+ this.capability = capability;
92
+ this.by = by;
93
+ Object.setPrototypeOf(this, new.target.prototype);
94
+ }
95
+ }
@@ -21,23 +21,199 @@ export type AdminMobileHostId = 'custom-android' | 'capacitor-ios' | 'capacitor-
21
21
  * Discriminated-union of all events the host may emit via
22
22
  * `AdminMobileHostContext.events.subscribe`.
23
23
  */
24
- export type AdminMobileEvent = {
24
+ export type AdminMobileEvent =
25
+ /**
26
+ * A tag was tapped while NO `requestNfcTap()` was waiting (a waiting request takes the tap instead).
27
+ * `uid` is uppercase hex without separators (e.g. `04A1B2C3D4E5F6`); `ndef` is the tag's NDEF text
28
+ * (usually its URL), `""` when the tag had none or it couldn't be read.
29
+ */
30
+ {
25
31
  type: 'nfc-tap';
26
32
  uid: string;
27
33
  ndef?: string;
28
- } | {
34
+ timestamp?: number;
35
+ }
36
+ /**
37
+ * RFID reads while the reader is running. Hosts deliver reads as they come — typically ONE EPC per
38
+ * event, many per second while the trigger is held — so collect and de-duplicate them yourself.
39
+ * `rssi` (dBm, negative; closer = higher) is the signal strength of the read when known.
40
+ */
41
+ | {
29
42
  type: 'rfid-burst';
30
43
  epcs: string[];
44
+ rssi?: number;
45
+ timestamp?: number;
31
46
  } | {
32
47
  type: 'qr-scan';
33
48
  code: string;
34
- } | {
49
+ timestamp?: number;
50
+ }
51
+ /**
52
+ * A hardware key. Hosts that report both edges send `action: 'down'` then `'up'` for one press —
53
+ * act on `'down'` (or on events with no `action`, from hosts that report presses only).
54
+ * 293 is the handheld scan trigger.
55
+ */
56
+ | {
35
57
  type: 'key-press';
36
58
  keyCode: number;
59
+ action?: 'down' | 'up';
60
+ timestamp?: number;
37
61
  } | {
38
62
  type: 'lifecycle';
39
63
  phase: 'pause' | 'resume' | 'offline' | 'online';
64
+ timestamp?: number;
40
65
  };
66
+ /**
67
+ * Options for `host.actions.requestNfcTap`.
68
+ *
69
+ * The request is NON-BLOCKING: it only waits for a tag. Keep the screen usable while it waits (e.g.
70
+ * offer "enter the code instead") and abort the wait with `signal` if the user goes another way.
71
+ */
72
+ export interface NfcTapRequestOptions {
73
+ /** Give up after this long with `HostTimeoutError`. Default: wait until tapped or aborted. */
74
+ timeoutMs?: number;
75
+ /** Abort the wait (rejects with `HostCancelledError`, `by: 'app'`). */
76
+ signal?: AbortSignal;
77
+ /** Short hint the host shows while waiting, e.g. "Tap the bottle's tag". Host default if omitted. */
78
+ prompt?: string;
79
+ }
80
+ /** Options for `host.actions.requestQrScan`. */
81
+ export interface QrScanRequestOptions {
82
+ /** Close the scanner (rejects with `HostCancelledError`, `by: 'app'`). */
83
+ signal?: AbortSignal;
84
+ /** Fraction of the screen the camera view covers, from the bottom (0.3–1). Default 1 (full screen). */
85
+ heightPercent?: number;
86
+ /** Short hint shown over the camera view. */
87
+ prompt?: string;
88
+ }
89
+ /** One RFID tag read (or, when batched, one tag's reads within the batch). */
90
+ export interface RfidRead {
91
+ /** The tag's EPC, uppercase hex. */
92
+ epc: string;
93
+ /** Manufacturer-burned tag ID, when the reader reports it. */
94
+ tid?: string;
95
+ /** Signal strength in dBm (negative; closer = higher). Best (highest) in the batch. */
96
+ rssi?: number;
97
+ /** When it was (last) read, ms since epoch. */
98
+ timestamp: number;
99
+ /** How many times this tag was read in the batch. */
100
+ count: number;
101
+ }
102
+ /** The reader as the host sees it. */
103
+ export interface RfidReaderStatus {
104
+ /** A UHF reader is selected on this device. */
105
+ available: boolean;
106
+ /** It's connected and ready (a Bluetooth reader may be available but not connected). */
107
+ connected: boolean;
108
+ /** Which reader: the device's built-in one or an external one. */
109
+ reader: 'built-in' | 'zebra' | 'tsl' | 'other' | null;
110
+ /** The reader is reading right now. */
111
+ reading: boolean;
112
+ /** Transmit power, when the reader supports changing it. */
113
+ power?: {
114
+ level: number;
115
+ min: number;
116
+ max: number;
117
+ };
118
+ }
119
+ /** Which tags a session reports. Reads of any other tag are dropped. */
120
+ export interface RfidFilter {
121
+ /** Only these EPCs. */
122
+ epcs?: string[];
123
+ /** Only EPCs starting with this (hex, e.g. your company prefix). */
124
+ prefix?: string;
125
+ }
126
+ /** Options for `host.rfid.read()`. */
127
+ export interface RfidReadOptions {
128
+ filter?: RfidFilter;
129
+ /**
130
+ * `'session'` (default): each tag is reported once per session. `{ windowMs }`: again only after it
131
+ * hasn't been read for that long. `'none'`: every read.
132
+ */
133
+ dedupe?: 'session' | 'none' | {
134
+ windowMs: number;
135
+ };
136
+ /** Deliver reads in batches every this many ms (default 250). `0` = each read as it arrives. */
137
+ batchMs?: number;
138
+ /**
139
+ * `'trigger'` (default): the reader runs while the operator holds the handheld's trigger.
140
+ * `'continuous'`: it runs from now until `stop()`.
141
+ */
142
+ mode?: 'trigger' | 'continuous';
143
+ /** Transmit power for this session (see `status().power`); restored afterwards. */
144
+ power?: number;
145
+ }
146
+ /** A reading session from `host.rfid.read()`. One session at a time: a new one stops the previous. */
147
+ export interface RfidReadSession {
148
+ /** Reads, batched per `batchMs`. Returns an unsubscribe function. */
149
+ onRead(cb: (reads: RfidRead[]) => void): () => void;
150
+ /** Distinct tags reported so far. */
151
+ readonly count: number;
152
+ /** Stop reading and end the session. */
153
+ stop(): void;
154
+ }
155
+ /** Options for `host.rfid.readOne()`. */
156
+ export interface RfidReadOneOptions {
157
+ filter?: RfidFilter;
158
+ /** Give up after this long with `HostTimeoutError`. Default: until a tag is read or aborted. */
159
+ timeoutMs?: number;
160
+ /** Abort (rejects with `HostCancelledError`, `by: 'app'`). */
161
+ signal?: AbortSignal;
162
+ /** Short hint the host shows while waiting, e.g. "Pull the trigger over the item". */
163
+ prompt?: string;
164
+ /** `'trigger'` (default) waits for the operator's trigger; `'continuous'` reads straight away. */
165
+ mode?: 'trigger' | 'continuous';
166
+ }
167
+ /** Options for `host.rfid.locate()`. */
168
+ export interface RfidLocateOptions {
169
+ /** The host plays faster beeps and haptics as the tag gets closer (default true). */
170
+ feedback?: boolean;
171
+ /** Transmit power while locating (lower narrows the field when you're close). */
172
+ power?: number;
173
+ }
174
+ /** How close the hunted tag is, updated several times a second. */
175
+ export interface RfidLocateUpdate {
176
+ /** The target tag last read (when hunting several), or null if none read yet. */
177
+ epc: string | null;
178
+ /** 0 (not in range) to 100 (right here), from signal strength and read rate. */
179
+ strength: number;
180
+ /** Latest signal strength in dBm, or null. */
181
+ rssi: number | null;
182
+ /** Reads of the target per second, over the last second. */
183
+ hitsPerSecond: number;
184
+ /** When it was last read, ms since epoch, or null. */
185
+ lastSeen: number | null;
186
+ }
187
+ /** A locate session from `host.rfid.locate()`. It reads continuously until `stop()`. */
188
+ export interface RfidLocateSession {
189
+ onUpdate(cb: (update: RfidLocateUpdate) => void): () => void;
190
+ stop(): void;
191
+ }
192
+ /**
193
+ * The host's UHF RFID reader — the same reading, single-read and find abilities the host's own scanner
194
+ * uses, for a container to build its own scanning screens. All three run on the one reader: starting
195
+ * any of them ends the one before. Everything stops when the container unmounts.
196
+ */
197
+ export interface AdminMobileRfid {
198
+ /** The reader's current state. */
199
+ status(): RfidReaderStatus;
200
+ /** Called whenever the status changes. Returns an unsubscribe function. */
201
+ onStatus(cb: (status: RfidReaderStatus) => void): () => void;
202
+ /** Keep reading tags — a stock take, an intake — de-duplicated and batched. */
203
+ read(opts?: RfidReadOptions): RfidReadSession;
204
+ /**
205
+ * "This step needs a tag": resolve with one tag (the strongest read in the first moment of reading),
206
+ * then stop. The RFID counterpart of `requestNfcTap`.
207
+ */
208
+ readOne(opts?: RfidReadOneOptions): Promise<RfidRead>;
209
+ /** Find one tag (or any of several, e.g. all tags on one item) by signal strength — a Geiger counter. */
210
+ locate(epcs: string | string[], opts?: RfidLocateOptions): RfidLocateSession;
211
+ }
212
+ /** Options for `host.actions.requestCameraPhoto`. */
213
+ export interface CameraPhotoRequestOptions {
214
+ /** Abandon the capture (rejects with `HostCancelledError`, `by: 'app'`). */
215
+ signal?: AbortSignal;
216
+ }
41
217
  /** Callback invoked for every hardware event emitted by the host. */
42
218
  export type AdminMobileEventCallback = (event: AdminMobileEvent) => void;
43
219
  /**
@@ -105,24 +281,38 @@ export interface AdminMobileHostContext {
105
281
  /** Imperative hardware actions. */
106
282
  actions: {
107
283
  /**
108
- * Open the QR scanner UI and resolve with the decoded string.
109
- * Throws `HostCapabilityUnavailableError` if `'qr'` is not in `capabilities`.
110
- * Throws `HostTimeoutError` if the user dismisses without scanning.
284
+ * Open the host's camera scanner and resolve with the decoded QR / barcode text. A deliberate
285
+ * action: the scanner covers the screen (or `heightPercent` of it) until a code is read.
286
+ * Rejects with `HostCancelledError` when the user closes the scanner (`by: 'user'`) or `signal`
287
+ * aborts (`by: 'app'`); `HostCapabilityUnavailableError` / `HostPermissionDeniedError` when there's
288
+ * no camera or no permission.
111
289
  */
112
- requestQrScan: () => Promise<string>;
290
+ requestQrScan: (opts?: QrScanRequestOptions) => Promise<string>;
113
291
  /**
114
- * Await the next NFC tap and resolve with uid + optional NDEF payload.
115
- * @param timeoutMs - Milliseconds before `HostTimeoutError` is thrown (host default if omitted).
292
+ * "This step needs a tag": wait for the NEXT NFC tap and resolve with its uid + NDEF.
293
+ *
294
+ * Two ways to use NFC — pick per screen:
295
+ * - Passive: `host.events` `nfc-tap` fires for every tap, whenever. For "tap anything, any time".
296
+ * - Active (this): the next tap goes to this request and is NOT also sent as an `nfc-tap` event, so
297
+ * nothing handles it twice. The host shows a small non-blocking hint (with Cancel) while waiting;
298
+ * the screen stays usable — abort with `signal` if the user takes another route.
299
+ * Only a tap made AFTER the call counts. One request at a time: a new one cancels the previous
300
+ * (`HostCancelledError`, `by: 'app'`).
301
+ * Rejects with `HostTimeoutError` after `timeoutMs`, `HostCancelledError` on Cancel (`by: 'user'`)
302
+ * or abort (`by: 'app'`), `HostCapabilityUnavailableError` without NFC.
303
+ *
304
+ * A bare number is accepted as `{ timeoutMs }` (the earlier signature).
116
305
  */
117
- requestNfcTap: (timeoutMs?: number) => Promise<{
306
+ requestNfcTap: (opts?: NfcTapRequestOptions | number) => Promise<{
118
307
  uid: string;
119
308
  ndef?: string;
120
309
  }>;
121
310
  /**
122
- * Open the camera shutter once and resolve with the captured `Blob`.
123
- * Throws `HostCapabilityUnavailableError` if `'camera'` is not in `capabilities`.
311
+ * Open the camera once and resolve with the photo as a `Blob`.
312
+ * Rejects with `HostCancelledError` when the user backs out or `signal` aborts;
313
+ * `HostCapabilityUnavailableError` / `HostPermissionDeniedError` without a camera or permission.
124
314
  */
125
- requestCameraPhoto: () => Promise<Blob>;
315
+ requestCameraPhoto: (opts?: CameraPhotoRequestOptions) => Promise<Blob>;
126
316
  /**
127
317
  * Trigger the native share sheet.
128
318
  * Falls back to clipboard write on hosts that do not implement the Web Share API.
@@ -166,6 +356,13 @@ export interface AdminMobileHostContext {
166
356
  */
167
357
  navigateBack?: () => void;
168
358
  };
359
+ /**
360
+ * The UHF RFID reader. Present only on hosts with one (`hardware.rfid`); feature-detect:
361
+ * @example
362
+ * const session = host.rfid?.read({ filter: { prefix: '3034' } });
363
+ * session?.onRead((reads) => addToCount(reads));
364
+ */
365
+ rfid?: AdminMobileRfid;
169
366
  /** Network connectivity helpers. */
170
367
  network: {
171
368
  /** Returns whether the device currently has network access. */
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.43 | Generated: 2026-10-06T15:37:31.849Z
3
+ Version: 2.0.45 | Generated: 2026-10-06T17:22:34.344Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -86,11 +86,11 @@ interface AdminMobileHostContext {
86
86
  // Unified hardware event stream — callback type: AdminMobileEventCallback
87
87
  events: { subscribe: (cb: AdminMobileEventCallback) => () => void }
88
88
 
89
- // Promise-based hardware actions — reject with a structured error when unavailable
89
+ // Promise-based hardware actions — reject with a structured error when unavailable or cancelled
90
90
  actions: {
91
- requestQrScan: () => Promise<string>
92
- requestNfcTap: (timeoutMs?: number) => Promise<{ uid: string; ndef?: string }>
93
- requestCameraPhoto: () => Promise<Blob>
91
+ requestQrScan: (opts?: { signal?: AbortSignal; heightPercent?: number; prompt?: string }) => Promise<string>
92
+ requestNfcTap: (opts?: { timeoutMs?: number; signal?: AbortSignal; prompt?: string } | number) => Promise<{ uid: string; ndef?: string }>
93
+ requestCameraPhoto: (opts?: { signal?: AbortSignal }) => Promise<Blob>
94
94
  share: (payload: { title: string; url: string; text?: string }) => Promise<void>
95
95
  clipboard: {
96
96
  read: () => Promise<string>
@@ -106,6 +106,9 @@ interface AdminMobileHostContext {
106
106
  navigateBack?: () => void
107
107
  }
108
108
 
109
+ // UHF RFID reader — only on hosts with one (feature-detect). See "RFID" below.
110
+ rfid?: AdminMobileRfid
111
+
109
112
  // Network & device info
110
113
  network: { isOnline: () => boolean }
111
114
  device: { info: () => Promise<{ model: string; platform: string }> }
@@ -331,6 +334,92 @@ The SmartLinks Mobile launcher loads your `mobileAdmin` bundle through the platf
331
334
 
332
335
  ---
333
336
 
337
+ ## Scanning: listening vs asking
338
+
339
+ Every scan reaches a container in one of two ways. Pick per screen.
340
+
341
+ | | Listening — `host.events` | Asking — `host.actions.request*` |
342
+ |---|---|---|
343
+ | Use for | "Tap or scan anything, any time" — a stock take, a scan-to-open list | "This step needs a scan" — tag this item, scan the shelf code |
344
+ | What the user sees | Nothing changes | NFC: a small non-blocking hint with Cancel. QR / photo: the host's camera opens |
345
+ | What you get | An event per tap / read / key | A promise for one result |
346
+
347
+ ### NFC: two modes
348
+
349
+ - **Listening** — subscribe to `nfc-tap`. Fires for every tap while no request is waiting.
350
+ - **Asking** — `await host.actions.requestNfcTap({ prompt: 'Tap the bottle tag', signal })`. The next tap
351
+ **made after the call** resolves it, and that tap is **not** also sent as an `nfc-tap` event, so
352
+ nothing handles it twice. The request is non-blocking: the screen stays usable while it waits — show
353
+ your own "or type the code" path and abort the wait with `signal` if the user takes it. One request at
354
+ a time: a new call cancels the previous one.
355
+
356
+ ```typescript
357
+ const controller = new AbortController()
358
+ try {
359
+ const { uid } = await host.actions.requestNfcTap({ prompt: 'Tap the bottle tag', signal: controller.signal })
360
+ await attachTag(uid)
361
+ } catch (err) {
362
+ if (err instanceof HostCancelledError) return // they pressed Cancel, or we aborted
363
+ throw err
364
+ }
365
+ // elsewhere — the user typed the code instead:
366
+ controller.abort()
367
+ ```
368
+
369
+ ### QR and photos
370
+
371
+ `requestQrScan()` opens the host's camera scanner (full screen, or the bottom `heightPercent` of it) and
372
+ resolves with the code; `requestCameraPhoto()` opens the camera and resolves with the image. Both reject
373
+ with `HostCancelledError` when the user backs out.
374
+
375
+ ### RFID
376
+
377
+ `host.rfid` gives a container the same reader abilities the host's own scanner uses — so an app can be
378
+ its own scanner. It's present only when the device has a UHF reader (`hardware.rfid`); check for it.
379
+ There's one reader, so starting any of the three below ends the one before, and everything stops when
380
+ the container unmounts.
381
+
382
+ **Keep reading** — a stock take, an intake. De-duplicated and batched, so you get each tag once instead
383
+ of hundreds of raw reads a second:
384
+
385
+ ```typescript
386
+ const session = host.rfid?.read({
387
+ filter: { prefix: '3034' }, // optional: only EPCs starting with this (or { epcs: [...] })
388
+ dedupe: 'session', // default — each tag once; { windowMs: 5000 } or 'none' for every read
389
+ batchMs: 250, // default — reads arrive in batches; 0 = one at a time
390
+ mode: 'trigger', // default — reads while the operator holds the trigger; 'continuous' = now until stop()
391
+ })
392
+ const off = session?.onRead((reads) => { // [{ epc, tid?, rssi?, timestamp, count }]
393
+ for (const r of reads) addToCount(r.epc)
394
+ })
395
+ // later
396
+ session?.stop()
397
+ ```
398
+
399
+ **One tag for this step** — tag an item. Resolves with the strongest tag read in the first moment of
400
+ reading, then stops; the RFID counterpart of `requestNfcTap` (non-blocking, abortable, `prompt` hint):
401
+
402
+ ```typescript
403
+ const { epc } = await host.rfid.readOne({ prompt: 'Pull the trigger over the item', signal })
404
+ ```
405
+
406
+ **Find a tag** — a Geiger counter. `strength` runs 0 (not in range) to 100 (right here); by default the
407
+ host plays faster beeps and haptics as you close in (`feedback: false` to do your own):
408
+
409
+ ```typescript
410
+ const hunt = host.rfid?.locate(item.epcs) // one EPC, or every tag on the item
411
+ hunt?.onUpdate(({ strength, rssi, hitsPerSecond }) => setStrength(strength))
412
+ // hunt.stop() when found
413
+ ```
414
+
415
+ `status()` / `onStatus(cb)` report whether a reader is available and connected, which one (`built-in`,
416
+ `zebra`, `tsl`), whether it's reading, and its power range; `read`/`locate` take a `power` level for the
417
+ session.
418
+
419
+ Raw reads still arrive as `rfid-burst` events whenever the reader runs, for containers that want them.
420
+
421
+ ---
422
+
334
423
  ## Event Stream
335
424
 
336
425
  For raw scan events (rather than the action-driven `host.actions.requestNfcTap`), subscribe to the unified event stream. Events flow identically on every host — the launcher normalises bridge messages, Capacitor callbacks, and Web API events into the same shape.
@@ -350,6 +439,13 @@ useEffect(() => {
350
439
  }, [host.events])
351
440
  ```
352
441
 
442
+ Event details:
443
+
444
+ - Every event may carry `timestamp` (ms since epoch).
445
+ - `nfc-tap` — `uid` is uppercase hex without separators (`04A1B2C3D4E5F6`); `ndef` is the tag's NDEF text (usually its URL), `""` when it had none.
446
+ - `rfid-burst` — reads arrive as they happen, typically **one EPC per event, many per second** while the trigger is held. Collect and de-duplicate them yourself. `rssi` (dBm; closer = higher) when the reader reports it.
447
+ - `key-press` — hosts that report both edges send `action: 'down'` then `'up'` for one press; act on `'down'` (or on events without `action`). `293` is the scan trigger.
448
+
353
449
  Lifecycle events (`event.type === 'lifecycle'`) carry a `phase` field: `'pause' | 'resume' | 'offline' | 'online'`. Use these instead of `window` event listeners so your container works identically on all hosts.
354
450
 
355
451
  ---
@@ -372,8 +468,15 @@ class HostTimeoutError extends Error {
372
468
  capability: 'nfc' | 'qr'
373
469
  timeoutMs: number
374
470
  }
471
+
472
+ class HostCancelledError extends Error {
473
+ capability: 'nfc' | 'qr' | 'camera'
474
+ by: 'user' | 'app' // closed the scanner / pressed Cancel, or aborted by the container
475
+ }
375
476
  ```
376
477
 
478
+ All four are exported from `@proveanything/smartlinks`; check with `instanceof` or `err.name`.
479
+
377
480
  ```typescript
378
481
  try {
379
482
  const code = await host.actions.requestQrScan()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.0.43",
3
+ "version": "2.0.45",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",