@proveanything/smartlinks 2.0.42 → 2.0.44
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/mobile-admin-container.md +107 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/mobile-admin/errors.d.ts +22 -1
- package/dist/mobile-admin/errors.js +23 -0
- package/dist/mobile-admin/types.d.ts +210 -13
- package/docs/API_SUMMARY.md +1 -1
- package/docs/mobile-admin-container.md +107 -4
- package/package.json +1 -1
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -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
|
|
109
|
-
*
|
|
110
|
-
*
|
|
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
|
-
*
|
|
115
|
-
*
|
|
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: (
|
|
306
|
+
requestNfcTap: (opts?: NfcTapRequestOptions | number) => Promise<{
|
|
118
307
|
uid: string;
|
|
119
308
|
ndef?: string;
|
|
120
309
|
}>;
|
|
121
310
|
/**
|
|
122
|
-
* Open the camera
|
|
123
|
-
*
|
|
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. */
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -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()
|