@proveanything/smartlinks 1.11.2 → 1.11.5

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: 1.11.2 | Generated: 2026-04-30T13:11:24.914Z
3
+ Version: 1.11.5 | Generated: 2026-04-30T16:31:53.616Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  This document describes how to build a **Mobile Admin Container** — a SmartLinks microapp that provides an in-the-field operator/admin surface optimised for mobile devices. These containers ship as a **separate `mobileAdmin` bundle** (not inside the `containers` bundle) so that Capacitor plugins, offline helpers, and operator-only code never reach the public consumer bundle.
6
6
 
7
- > **See also:** [containers.md](containers.md) covers the public consumer container. The [Multiple Consumer Components](containers.md#multiple-consumer-components) section explains the consumer vs. admin bundle split.
7
+ > **See also:** [containers.md](containers.md) covers the public consumer container. The [Multiple Consumer Components](containers.md#multiple-consumer-components) section explains the consumer vs. admin bundle split. For the full `host.native` facade contract (share, clipboard, haptics, NFC, RFID, storage, etc.) see [native-facade.md](native-facade.md).
8
8
 
9
9
  ---
10
10
 
@@ -23,6 +23,7 @@ This document describes how to build a **Mobile Admin Container** — a SmartLin
23
23
  11. [Build & Bundle Requirements](#build--bundle-requirements)
24
24
  12. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
25
25
  13. [Best Practices](#best-practices)
26
+ 14. [Native Capability Facade](#native-capability-facade)
26
27
 
27
28
  ---
28
29
 
@@ -58,7 +59,7 @@ Your container **never** detects the host directly. It receives a `host` prop fr
58
59
 
59
60
  Every container mounted by the SmartLinks Mobile launcher receives a single `host` prop — `AdminMobileHostContext`. Do not reach for `window.SmartlinksScanner` or `window.Capacitor` directly; both are wrapped here.
60
61
 
61
- > **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `ScannerEventSubscriber`, `MobileAdminComponentManifest`, and `MobileAdminBundleManifest` are all exported from `@proveanything/smartlinks`. Import via `import type { AdminMobileHostContext } from '@proveanything/smartlinks'` — no local mirror needed.
62
+ > **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `ActionableCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `AdminMobileEventCallback`, `AdminMobileEventSubscriber`, `AdminMobileComponentManifest`, `AdminMobileBundleManifest`, and `NativeFacade` (plus all sub-facade interfaces) are exported from `@proveanything/smartlinks`. Import via `import type { AdminMobileHostContext } from '@proveanything/smartlinks'` — no local mirror needed. `ScannerEventSubscriber`, `MobileAdminComponentManifest`, and `MobileAdminBundleManifest` still export as deprecated aliases.
62
63
 
63
64
  ```typescript
64
65
  interface AdminMobileHostContext {
@@ -82,8 +83,8 @@ interface AdminMobileHostContext {
82
83
  keyboard: boolean
83
84
  }
84
85
 
85
- // Unified hardware event stream
86
- events: { subscribe: ScannerEventSubscriber }
86
+ // Unified hardware event stream — callback type: AdminMobileEventCallback
87
+ events: { subscribe: (cb: AdminMobileEventCallback) => () => void }
87
88
 
88
89
  // Promise-based hardware actions — reject with a structured error when unavailable
89
90
  actions: {
@@ -97,12 +98,12 @@ interface AdminMobileHostContext {
97
98
  }
98
99
  }
99
100
 
100
- // Host-provided UI helpers
101
+ // Host-provided UI helpers — all optional, see §3 note
101
102
  ui: {
102
- toast: (opts: { title: string; description?: string; variant?: 'default' | 'destructive' }) => void
103
- haptic: (style?: 'light' | 'success' | 'error') => void
104
- setHeaderTitle: (title: string | null) => void
105
- navigateBack: () => void
103
+ toast?: (opts: { title: string; description?: string; variant?: 'default' | 'destructive' }) => void
104
+ haptic?: (style?: 'light' | 'success' | 'error') => void
105
+ setHeaderTitle?: (title: string | null) => void
106
+ navigateBack?: () => void
106
107
  }
107
108
 
108
109
  // Network & device info
@@ -112,6 +113,10 @@ interface AdminMobileHostContext {
112
113
  // Informational host version — use for logging/diagnostics, not feature detection.
113
114
  // For feature detection prefer existence checks: 'requestNfcTap' in host.actions
114
115
  _version: number
116
+
117
+ // Full native capability facade — share, clipboard, haptics, NFC, RFID, storage, etc.
118
+ // Optional: not every host stub populates it. See native-facade.md.
119
+ native?: NativeFacade
115
120
  }
116
121
  ```
117
122
 
@@ -143,9 +148,10 @@ if (host.hardware.nfc) {
143
148
 
144
149
  ### `host.ui` — native helpers vs. your own components
145
150
 
146
- `host.ui.setHeaderTitle()` and `host.ui.navigateBack()` are **host-only**there is no in-container equivalent. Call them via `host.ui` or omit them.
151
+ All four `host.ui` methods are **optional by design**. A container may run in the Sidekick mobile shell, a standalone PWA or browser tab, Storybook, or a screenshot harness none of those environments is guaranteed to have a native toast system, a managed header, or a back stack. Forcing every host implementer to provide them would exclude the web and desktop use-cases the rest of the contract explicitly supports. Always guard with `?.`:
147
152
 
148
- `host.ui.toast()` and `host.ui.haptic()` are **optional conveniences**. Use them when you want native system feedback. When rendering in Storybook, unit tests, or a plain browser tab, your own `<Toaster />` is a perfectly valid substitute — you do not need to stub the entire `host.ui` surface just to get toast notifications.
153
+ - `host.ui.toast?.({...})` / `host.ui.haptic?.('success')` optional native feedback. Fall back to your own `<Toaster />` when absent.
154
+ - `host.ui.setHeaderTitle?.('Scanning…')` / `host.ui.navigateBack?.()` — host-shell integrations with no in-container equivalent. Silently no-op when the host doesn't implement them.
149
155
 
150
156
  **Stub pattern for testing and Storybook:**
151
157
 
@@ -222,12 +228,23 @@ The custom Kotlin shell and both Capacitor shells ship the same baseline plugin
222
228
  | `@capacitor/device` | `host.device.info()` |
223
229
  | `@capacitor/share` | `host.actions.share()` |
224
230
  | `@capacitor/clipboard` | `host.actions.clipboard.*` |
225
- | `@capacitor/preferences` | `host.storage.*` *(planned)* |
231
+ | `@capacitor/preferences` | `host.storage.*` *(planned — see note below)* |
226
232
  | `@capacitor/app` | host-managed (back button, deep links) |
227
233
  | `@capacitor/status-bar` | host-managed |
228
234
  | `@capacitor/keyboard` | host-managed |
229
235
  | `@capacitor/toast` | wired into `host.ui.toast()` |
230
236
 
237
+ > **`host.storage` — planned shape.** Once released, the surface will wrap `@capacitor/preferences` directly:
238
+ > ```ts
239
+ > host.storage: {
240
+ > get(key: string): Promise<string | null>
241
+ > set(key: string, value: string): Promise<void>
242
+ > remove(key: string): Promise<void>
243
+ > keys(): Promise<string[]>
244
+ > }
245
+ > ```
246
+ > Until then: `localStorage` works on web hosts; use `@capacitor/preferences` directly (bundle it in) on native.
247
+
231
248
  ### Tier 2 — capability-gated (declare in manifest)
232
249
 
233
250
  | Plugin | Capability flag |
@@ -385,7 +402,9 @@ try {
385
402
  | `lifecycle: 'pause'` | App backgrounded | Pause readers to save battery |
386
403
  | `lifecycle: 'resume'` | App foregrounded | Resubscribe; refresh stale data |
387
404
 
388
- Use `host.events.subscribe` for all lifecycle events — it fires identically on every host.
405
+ Use `host.events.subscribe` for all lifecycle events — all five host types (`custom-android`, `capacitor-ios`, `capacitor-android`, `pwa`, `browser`) are guaranteed to emit every `'pause'`/`'resume'`/`'offline'`/`'online'` phase. No `window.addEventListener('online')` fallback is needed.
406
+
407
+ > **Planned** — `phase: 'mount' | 'unmount'` events are on the roadmap. These will fire when the container becomes visible / is removed from the host view stack, enabling deferred reader start-up without a `useEffect` dependency.
389
408
 
390
409
  ---
391
410
 
@@ -521,10 +540,33 @@ export const MOBILE_ADMIN_MANIFEST = {
521
540
 
522
541
  - **Always check `host.hardware.X` before calling `host.actions.X`** — never assume a capability is available.
523
542
  - **Always wrap action calls in try/catch** — handle `HostPermissionDeniedError`, `HostTimeoutError`, and `HostCapabilityUnavailableError`.
524
- - **Use `host.ui.setHeaderTitle` and `host.ui.navigateBack`** for header integrationthese are host-only and have no in-container equivalent.
525
- - **`host.ui.toast` and `host.ui.haptic` are optional** use them for native feedback, or fall back to your own `<Toaster />` when testing in isolation.
543
+ - **All four `host.ui` methods are optional** (`toast`, `haptic`, `setHeaderTitle`, `navigateBack`)guard every call with `?.`. See the `host.ui` section above.
544
+ - **`host.ui.setHeaderTitle` and `host.ui.navigateBack`** integrate with the host shell and have no in-container equivalent; call with `?.`.
526
545
  - **Use `host.events.subscribe` for lifecycle events** — `'offline'`/`'online'`/`'pause'`/`'resume'` fire consistently on every host.
527
546
  - **Never call `initializeApi`, never use top-level SDK imports for API calls** — `host.SL` is already configured. `SL.method()` instead of `host.SL.method()` silently uses the wrong baseURL.
528
547
  - **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
529
548
  - **Declare `offline: true`** on a component if it queues writes locally — this signals the launcher to provision offline sync support.
530
549
  - **Use `'methodName' in host.actions`** to feature-detect new host capabilities rather than comparing `host._version`. The version is informational only.
550
+
551
+ ---
552
+
553
+ ## Native Capability Facade
554
+
555
+ `host.native` gives containers access to a broader set of device capabilities — share sheet, clipboard, full haptics API, storage, QR, NFC, RFID (Kotlin only), auth, and cross-shell events — through a single interface that falls back gracefully across all host environments.
556
+
557
+ ```typescript
558
+ // Check host.hardware.* for physical availability, then call via host.native
559
+ if (host.hardware.nfc && host.native?.nfc) {
560
+ const { uid } = await host.native.nfc.read({ timeoutMs: 10_000 })
561
+ }
562
+
563
+ // Storage always available (Preferences → localStorage → in-memory Map)
564
+ await host.native?.storage.set('lastScan', uid)
565
+
566
+ // Share sheet with web fallback
567
+ await host.native?.share.share({ title: 'Found tag', url: `https://app.example/tags/${uid}` })
568
+ ```
569
+
570
+ `host.native` is optional on `AdminMobileHostContext` — host stubs (Storybook, unit tests) need not implement it. `native.rfid` is additionally optional within `NativeFacade` itself, as it only exists on `custom-android`.
571
+
572
+ For the full sub-facade table, fallback chains, and what's intentionally not wrapped, see **[native-facade.md](native-facade.md)**.
@@ -0,0 +1,170 @@
1
+ # Native Capability Facade (`host.native` / `SL.native`)
2
+
3
+ > **Version:** 1.12 · **Platform:** SmartLinks R4 · **Last updated:** 2026-04-30
4
+
5
+ The `NativeFacade` is a thin contract layer between microapps and the device capabilities available on the current host shell (Kotlin, Capacitor iOS/Android, PWA, or browser). It lets a microapp call `host.native.share.share({...})` without knowing whether it's running over `window.SmartlinksScanner`, a Capacitor plugin, or `navigator.share`.
6
+
7
+ **The SDK owns the contract** (`src/native/types.ts`, re-exported from `@proveanything/smartlinks`). Each host implementation is responsible for wiring up the sub-facades and populating:
8
+ - `AdminMobileHostContext.native` (container prop, typed in the SDK), and
9
+ - `window.SL.native` (for UMD microapps that don't receive a host prop — host concern, not SDK).
10
+
11
+ ---
12
+
13
+ ## Table of Contents
14
+
15
+ 1. [Access patterns](#access-patterns)
16
+ 2. [What's in the facade](#whats-in-the-facade)
17
+ 3. [What's NOT in the facade](#whats-not-in-the-facade)
18
+ 4. [Error contract](#error-contract)
19
+ 5. [Type reference](#type-reference)
20
+
21
+ ---
22
+
23
+ ## Access patterns
24
+
25
+ ### Inside a Mobile Admin Container (recommended)
26
+
27
+ ```typescript
28
+ import type { AdminMobileHostContext } from '@proveanything/smartlinks'
29
+
30
+ function MyContainer({ host }: { host: AdminMobileHostContext }) {
31
+ const handleShare = async () => {
32
+ await host.native?.share.share({ title: 'SmartLinks', url: window.location.href })
33
+ }
34
+
35
+ const handleScan = async () => {
36
+ // Only present when the host provides a QR reader
37
+ const code = await host.native?.qr.scan()
38
+ if (code) processCode(code)
39
+ }
40
+ }
41
+ ```
42
+
43
+ Always call via `host.native?.<facade>.<method>()` — `native` is optional because not every host exposes the full surface (e.g. a minimal browser stub may omit `rfid`).
44
+
45
+ For hardware-gated capabilities (NFC, RFID, QR, camera) also check `host.capabilities` or `host.hardware.*` before calling, as those flags reflect physical availability on the current device:
46
+
47
+ ```typescript
48
+ if (host.hardware.nfc && host.native?.nfc) {
49
+ const { uid } = await host.native.nfc.read({ timeoutMs: 10_000 })
50
+ }
51
+ ```
52
+
53
+ ### Inside a UMD microapp (no host prop)
54
+
55
+ ```typescript
56
+ // window.SL.native is populated by the host at boot — host concern, not SDK
57
+ const native = (window as any).SL?.native as import('@proveanything/smartlinks').NativeFacade | undefined
58
+ const code = await native?.qr.scan()
59
+ ```
60
+
61
+ ---
62
+
63
+ ## What's in the facade
64
+
65
+ These capabilities have meaningful fallbacks across Kotlin / Capacitor / web. Always go through the facade.
66
+
67
+ | Sub-facade | Key methods | Fallback chain |
68
+ |---|---|---|
69
+ | `native.share` | `share({ title, url, text? })`, `canShare()` | Capacitor Share → `navigator.share` → copy to clipboard + toast |
70
+ | `native.clipboard` | `write(text)`, `read()` | Capacitor Clipboard → `navigator.clipboard` → `execCommand('copy')` |
71
+ | `native.haptics` | `impact(style?)`, `notification(style)`, `selection()` | Capacitor Haptics → `navigator.vibrate` → no-op |
72
+ | `native.network` | `getStatus()`, `addListener('change', cb)` | Capacitor Network → `navigator.onLine` + `'online'`/`'offline'` events |
73
+ | `native.device` | `getInfo()`, `getId()`, `getLanguageCode()` | Capacitor Device → UA parsing + cached UUID in `localStorage` |
74
+ | `native.storage` | `get(key)`, `set(key, value)`, `remove(key)`, `keys()` | Capacitor Preferences → `localStorage` → in-memory `Map` (private mode) |
75
+ | `native.qr` | `scan(opts?)` | Capacitor MLKit Barcode → html5-qrcode / `BarcodeDetector` |
76
+ | `native.auth` | `signInWithGoogle()`, `signOut()` | Kotlin bridge → Capacitor Browser + OAuth → web redirect |
77
+ | `native.nfc` | `read(opts?)`, `writeNdef(opts)`, `programTag(opts)`, `lockTag(opts)`, `isLocked(opts)` | Kotlin bridge → Web NFC (`NDEFReader`) → `HostCapabilityUnavailableError` |
78
+ | `native.rfid` | `startScan(opts?)`, `stopScan()`, `subscribe(cb)` | Kotlin bridge only — throws `HostCapabilityUnavailableError` on all other hosts |
79
+ | `native.events` | `subscribe(type, cb)`, `emit(type, payload?)` | Internal pub/sub bridging Kotlin `onSmartlinksData` + Capacitor listeners |
80
+ | `native.webSource` | `get()`, `set({ mode, channel?, liveUrl? })` | Kotlin bridge + Capacitor Preferences mirror |
81
+
82
+ > **`native.rfid` is the only optional sub-facade** on `NativeFacade` — it is only populated on `custom-android` hosts. All other sub-facades are present on every host, though individual methods may throw `HostCapabilityUnavailableError` when the underlying capability is absent.
83
+
84
+ > **`native.device` vs `host.device`** — `host.device.info()` is a simplified convenience that returns `{ model, platform }`. `host.native?.device.getInfo()` returns the full `DeviceInfo` (adds `osVersion`, `manufacturer`, `isVirtual`) and also provides `getId()` and `getLanguageCode()`. Use `host.device` when the convenience is enough.
85
+
86
+ > **`native.network` vs `host.network`** — `host.network.isOnline()` is a boolean convenience. `host.native?.network.getStatus()` returns `{ connected, connectionType }` including `'wifi'` / `'cellular'` distinction.
87
+
88
+ ---
89
+
90
+ ## What's NOT in the facade
91
+
92
+ These capabilities intentionally have no `SL.native` wrapper. Call the Capacitor plugin directly, or use the web API. The host provides them as Tier 1 or Tier 2 plugins (see [Capacitor Plugin Baseline](mobile-admin-container.md#capacitor-plugin-baseline)).
93
+
94
+ | Capability | Why not wrapped |
95
+ |---|---|
96
+ | `@capacitor/push-notifications` | Push only exists in Capacitor builds; no cross-shell registration flow. Import and call directly; detect Capacitor first. |
97
+ | `@capacitor/local-notifications` | Same — no Kotlin or web equivalent worth abstracting. |
98
+ | `@capacitor/camera` | Plugin already abstracts iOS/Android. PWA's `<input capture>` is too different in shape to unify. |
99
+ | `@capacitor/filesystem` | No web equivalent (storage quotas, sandbox rules differ). Use `URL.createObjectURL` + download anchor on web. |
100
+ | `@capacitor/geolocation` | `navigator.geolocation` already works in WebViews; the Capacitor plugin wraps the same OS APIs. No facade adds value. |
101
+ | `@capacitor/keyboard`, `@capacitor/screen-*`, `@capacitor/splash-screen`, `@capacitor/status-bar`, `@capacitor/dialog`, `@capacitor/app` | Per-host boot-time config or platform-chrome concerns; not part of the microapp contract. |
102
+ | `@capacitor/browser` | In-app browser for OAuth is intentionally Capacitor-only. For opening external URLs use `native.share.share({ url })`. |
103
+ | `@capawesome/capacitor-file-picker` | Picker UX too divergent across web (`<input type="file">`) and native to wrap usefully. |
104
+
105
+ If a capability later needs consistent behaviour across shells (e.g. notifications reach enough hosts to warrant throttling / routing via the facade), promote it into `NativeFacade` by adding an interface to `src/native/types.ts` and a row to the table above.
106
+
107
+ ---
108
+
109
+ ## Error contract
110
+
111
+ All facade methods reject with one of the three structured errors from `@proveanything/smartlinks`. A single catch block handles every capability and host combination:
112
+
113
+ ```typescript
114
+ import {
115
+ HostCapabilityUnavailableError,
116
+ HostPermissionDeniedError,
117
+ HostTimeoutError,
118
+ } from '@proveanything/smartlinks'
119
+
120
+ try {
121
+ const { uid } = await host.native?.nfc.read({ timeoutMs: 10_000 }) ?? {}
122
+ } catch (err) {
123
+ if (err instanceof HostCapabilityUnavailableError) {
124
+ // host.capabilities won't list 'nfc' — we should have checked first
125
+ host.ui.toast?.({ title: 'NFC not available on this device', variant: 'destructive' })
126
+ } else if (err instanceof HostPermissionDeniedError) {
127
+ host.ui.toast?.({ title: 'NFC permission denied', variant: 'destructive' })
128
+ } else if (err instanceof HostTimeoutError) {
129
+ host.ui.toast?.({ title: `No tag detected after ${err.timeoutMs / 1000}s` })
130
+ } else {
131
+ throw err
132
+ }
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Type reference
139
+
140
+ All types below are exported from `@proveanything/smartlinks`:
141
+
142
+ ```typescript
143
+ import type {
144
+ NativeFacade, // root — use on AdminMobileHostContext.native
145
+ NativeCapability, // union of sub-facade keys: 'share' | 'clipboard' | ...
146
+ ShareFacade,
147
+ ClipboardFacade,
148
+ HapticsFacade,
149
+ HapticImpactStyle, // 'light' | 'medium' | 'heavy'
150
+ HapticNotificationStyle,
151
+ NetworkFacade,
152
+ NetworkStatus,
153
+ DeviceFacade,
154
+ DeviceInfo,
155
+ StorageFacade,
156
+ QrFacade,
157
+ QrScanOptions,
158
+ AuthFacade,
159
+ NfcFacade,
160
+ NfcReadResult,
161
+ RfidFacade,
162
+ RfidScanOptions,
163
+ EventsFacade,
164
+ WebSourceFacade,
165
+ WebSourceMode,
166
+ WebSourceConfig,
167
+ } from '@proveanything/smartlinks'
168
+ ```
169
+
170
+ The facade interfaces are contract-only — no runtime implementation is shipped in the SDK. You only need them for TypeScript type-checking (authoring containers or host stubs).
package/dist/index.d.ts CHANGED
@@ -23,5 +23,8 @@ export type { Collection, CollectionResponse, CollectionCreateRequest, Collectio
23
23
  export type { Proof, ProofResponse, ProofCreateRequest, ProofUpdateRequest, ProofClaimRequest, } from "./types/proof";
24
24
  export type { QrShortCodeLookupResponse, } from "./types/qr";
25
25
  export type { ReverseTagLookupParams, ReverseTagLookupResponse, } from "./types/tags";
26
- export type { AdminMobileCapability, AdminMobileHostId, AdminMobileEvent, ScannerEventSubscriber, AdminMobileHostContext, MobileAdminComponentManifest, MobileAdminBundleManifest, } from './mobile-admin/types';
26
+ export type { AdminMobileCapability, ActionableCapability, AdminMobileHostId, AdminMobileEvent, AdminMobileEventCallback, AdminMobileEventSubscriber, ScannerEventSubscriber, // @deprecated use AdminMobileEventCallback
27
+ AdminMobileHostContext, AdminMobileComponentManifest, AdminMobileBundleManifest, MobileAdminComponentManifest, // @deprecated — use AdminMobileComponentManifest
28
+ MobileAdminBundleManifest, } from './mobile-admin/types';
27
29
  export { HostCapabilityUnavailableError, HostPermissionDeniedError, HostTimeoutError, } from './mobile-admin/errors';
30
+ 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';
@@ -5,6 +5,7 @@
5
5
  * All three classes call `Object.setPrototypeOf(this, new.target.prototype)` so
6
6
  * `instanceof` works correctly when transpiled to ES5.
7
7
  */
8
+ import type { ActionableCapability, AdminMobileHostId } from './types';
8
9
  /**
9
10
  * Thrown when a container requests a hardware action that the current host
10
11
  * does not support (e.g. calling `requestNfcTap` on a `'pwa'` host).
@@ -20,10 +21,10 @@
20
21
  */
21
22
  export declare class HostCapabilityUnavailableError extends Error {
22
23
  /** The capability that was requested but is unavailable. */
23
- capability: 'nfc' | 'rfid' | 'qr' | 'camera';
24
- /** `AdminMobileHostId` string of the host that rejected the request. */
25
- host: string;
26
- constructor(capability: HostCapabilityUnavailableError['capability'], host: string);
24
+ capability: ActionableCapability;
25
+ /** The host on which the capability is unavailable. */
26
+ host: AdminMobileHostId;
27
+ constructor(capability: ActionableCapability, host: AdminMobileHostId);
27
28
  }
28
29
  /**
29
30
  * Thrown when the user denies a runtime permission request (e.g. camera or
@@ -40,8 +41,8 @@ export declare class HostCapabilityUnavailableError extends Error {
40
41
  */
41
42
  export declare class HostPermissionDeniedError extends Error {
42
43
  /** The capability for which permission was denied. */
43
- capability: 'nfc' | 'rfid' | 'qr' | 'camera';
44
- constructor(capability: HostPermissionDeniedError['capability']);
44
+ capability: ActionableCapability;
45
+ constructor(capability: ActionableCapability);
45
46
  }
46
47
  /**
47
48
  * Thrown when a time-bounded host action (NFC tap, QR scan) exceeds its
@@ -58,7 +59,7 @@ export declare class HostPermissionDeniedError extends Error {
58
59
  */
59
60
  export declare class HostTimeoutError extends Error {
60
61
  /** The capability that timed out. */
61
- capability: 'nfc' | 'qr';
62
+ capability: Extract<ActionableCapability, 'nfc' | 'qr' | 'geolocation'>;
62
63
  /** The timeout threshold in milliseconds. */
63
64
  timeoutMs: number;
64
65
  constructor(capability: HostTimeoutError['capability'], timeoutMs: number);
@@ -1,9 +1,16 @@
1
+ import type { NativeFacade } from '../native/types';
1
2
  /**
2
3
  * Hardware/software capability tokens a mobile admin host may advertise.
3
4
  * Passed to `AdminMobileHostContext.capabilities` and to
4
- * `MobileAdminComponentManifest.capabilities`.
5
+ * `AdminMobileComponentManifest.capabilities`.
5
6
  */
6
7
  export type AdminMobileCapability = 'nfc' | 'nfc-advanced' | 'rfid' | 'qr' | 'camera' | 'keyboard' | 'geolocation' | 'push';
8
+ /**
9
+ * Subset of `AdminMobileCapability` that can be the subject of a structured
10
+ * error. `'keyboard'` is excluded — it is a passive event source with no
11
+ * request method that can fail.
12
+ */
13
+ export type ActionableCapability = Exclude<AdminMobileCapability, 'keyboard'>;
7
14
  /**
8
15
  * Canonical identifiers for mobile admin host environments.
9
16
  * Use for display / diagnostics only — feature-detect at runtime via
@@ -31,8 +38,19 @@ export type AdminMobileEvent = {
31
38
  type: 'lifecycle';
32
39
  phase: 'pause' | 'resume' | 'offline' | 'online';
33
40
  };
34
- /** Callback signature for `AdminMobileHostContext.events.subscribe`. */
35
- export type ScannerEventSubscriber = (event: AdminMobileEvent) => void;
41
+ /** Callback invoked for every hardware event emitted by the host. */
42
+ export type AdminMobileEventCallback = (event: AdminMobileEvent) => void;
43
+ /**
44
+ * The full type of `AdminMobileHostContext.events.subscribe` —
45
+ * takes a callback and returns a cleanup function.
46
+ */
47
+ export type AdminMobileEventSubscriber = (cb: AdminMobileEventCallback) => () => void;
48
+ /**
49
+ * @deprecated Renamed to `AdminMobileEventCallback` in 1.12.
50
+ * Will be removed in a future minor release.
51
+ * @see AdminMobileEventCallback
52
+ */
53
+ export type ScannerEventSubscriber = AdminMobileEventCallback;
36
54
  /**
37
55
  * The `host` prop passed to every mobile admin container component.
38
56
  *
@@ -82,7 +100,7 @@ export interface AdminMobileHostContext {
82
100
  * @param cb - Called for every incoming `AdminMobileEvent`.
83
101
  * @returns Cleanup function — call inside `useEffect` return.
84
102
  */
85
- subscribe: (cb: ScannerEventSubscriber) => () => void;
103
+ subscribe: (cb: AdminMobileEventCallback) => () => void;
86
104
  };
87
105
  /** Imperative hardware actions. */
88
106
  actions: {
@@ -137,8 +155,16 @@ export interface AdminMobileHostContext {
137
155
  }) => void;
138
156
  /** Trigger a haptic pulse. Optional — see interface note. */
139
157
  haptic?: (style?: 'light' | 'success' | 'error') => void;
140
- setHeaderTitle: (title: string | null) => void;
141
- navigateBack: () => void;
158
+ /**
159
+ * Optional host shell may not provide a managed header
160
+ * (browser tabs, Storybook, desktop views). Guard with `?.`.
161
+ */
162
+ setHeaderTitle?: (title: string | null) => void;
163
+ /**
164
+ * Optional — host shell may not have a native back stack
165
+ * (browser tabs, Storybook, desktop views). Guard with `?.`.
166
+ */
167
+ navigateBack?: () => void;
142
168
  };
143
169
  /** Network connectivity helpers. */
144
170
  network: {
@@ -160,9 +186,19 @@ export interface AdminMobileHostContext {
160
186
  * if ('requestNfcTap' in host.actions) { ... }
161
187
  */
162
188
  _version: number;
189
+ /**
190
+ * Full native capability facade populated by the host at mount time.
191
+ * Not all sub-facades are present on every host — check before calling:
192
+ * @example
193
+ * const code = await host.native?.qr.scan();
194
+ * const { identifier } = await host.native?.device.getId() ?? {};
195
+ * For UMD microapps that do not receive a `host` prop, the same object
196
+ * is available at `window.SL.native` (host concern, not SDK).
197
+ */
198
+ native?: NativeFacade;
163
199
  }
164
200
  /** Manifest metadata for a single mobile admin container component. */
165
- export interface MobileAdminComponentManifest {
201
+ export interface AdminMobileComponentManifest {
166
202
  /** Component export name (matches the export in the bundle). */
167
203
  name: string;
168
204
  /** Human-readable description shown in the host launcher UI. */
@@ -184,7 +220,7 @@ export interface MobileAdminComponentManifest {
184
220
  * Shape of the `mobileAdmin` key inside `app.manifest.json`.
185
221
  * Describes the bundle files and the components it exports.
186
222
  */
187
- export interface MobileAdminBundleManifest {
223
+ export interface AdminMobileBundleManifest {
188
224
  files: {
189
225
  js: {
190
226
  /** UMD bundle path (relative to dist root). Used by the custom-android host. */
@@ -195,5 +231,15 @@ export interface MobileAdminBundleManifest {
195
231
  /** CSS bundle path, or `null` if the component ships no styles. */
196
232
  css: string | null;
197
233
  };
198
- components: MobileAdminComponentManifest[];
234
+ components: AdminMobileComponentManifest[];
199
235
  }
236
+ /**
237
+ * @deprecated Renamed to `AdminMobileComponentManifest` in 1.12.
238
+ * @see AdminMobileComponentManifest
239
+ */
240
+ export type MobileAdminComponentManifest = AdminMobileComponentManifest;
241
+ /**
242
+ * @deprecated Renamed to `AdminMobileBundleManifest` in 1.12.
243
+ * @see AdminMobileBundleManifest
244
+ */
245
+ export type MobileAdminBundleManifest = AdminMobileBundleManifest;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Contract types for the `SL.native` / `host.native` facade.
3
+ *
4
+ * These are **interface-only** — no runtime implementation lives here.
5
+ * The facade is implemented per-host (Sidekick, Capacitor builds, PWA) and
6
+ * exposed to containers via `AdminMobileHostContext.native` or (for UMD
7
+ * microapps) via `window.SL.native`. The SDK owns the shape; the host owns
8
+ * the wiring.
9
+ *
10
+ * All facade methods reject with one of the structured errors from
11
+ * `@proveanything/smartlinks` (`HostCapabilityUnavailableError`,
12
+ * `HostPermissionDeniedError`, `HostTimeoutError`) so a single catch handler
13
+ * covers every host and capability.
14
+ */
15
+ /**
16
+ * Keys of the `NativeFacade` interface — one token per sub-facade.
17
+ * Use `hasCapability(key)` on the host to check availability before calling.
18
+ */
19
+ export type NativeCapability = 'share' | 'clipboard' | 'haptics' | 'network' | 'device' | 'storage' | 'qr' | 'auth' | 'nfc' | 'rfid' | 'events' | 'webSource';
20
+ /** Share sheet / URL sharing. */
21
+ export interface ShareFacade {
22
+ /**
23
+ * Trigger the native share sheet.
24
+ * Falls back to `navigator.share`, then silently copies `url` to clipboard.
25
+ */
26
+ share(payload: {
27
+ title: string;
28
+ url: string;
29
+ text?: string;
30
+ }): Promise<void>;
31
+ /** Returns `true` when a share sheet is available on this host. */
32
+ canShare(): Promise<boolean>;
33
+ }
34
+ /** Clipboard read / write. */
35
+ export interface ClipboardFacade {
36
+ /** Write plain text to the clipboard. */
37
+ write(text: string): Promise<void>;
38
+ /** Read plain text from the clipboard. Returns `''` if empty or denied. */
39
+ read(): Promise<string>;
40
+ }
41
+ /** Haptic feedback. */
42
+ export type HapticImpactStyle = 'light' | 'medium' | 'heavy';
43
+ export type HapticNotificationStyle = 'success' | 'warning' | 'error';
44
+ export interface HapticsFacade {
45
+ /** Physical impact pulse. Falls back to `navigator.vibrate`. */
46
+ impact(style?: HapticImpactStyle): Promise<void>;
47
+ /** Notification-style feedback. Falls back to `navigator.vibrate`. */
48
+ notification(style: HapticNotificationStyle): Promise<void>;
49
+ /** Selection-changed feedback. No-op when unsupported. */
50
+ selection(): Promise<void>;
51
+ }
52
+ /** Network status. */
53
+ export interface NetworkStatus {
54
+ connected: boolean;
55
+ connectionType: 'wifi' | 'cellular' | 'none' | 'unknown';
56
+ }
57
+ export interface NetworkFacade {
58
+ /** Returns current connection status. */
59
+ getStatus(): Promise<NetworkStatus>;
60
+ /**
61
+ * Subscribe to connectivity changes. Returns an unsubscribe function.
62
+ * Falls back to `window` `'online'`/`'offline'` events on web.
63
+ */
64
+ addListener(event: 'change', cb: (status: NetworkStatus) => void): () => void;
65
+ }
66
+ /** Device metadata. */
67
+ export interface DeviceInfo {
68
+ model: string;
69
+ platform: 'ios' | 'android' | 'web';
70
+ osVersion: string;
71
+ manufacturer: string;
72
+ isVirtual: boolean;
73
+ }
74
+ export interface DeviceFacade {
75
+ /** Hardware and OS metadata. */
76
+ getInfo(): Promise<DeviceInfo>;
77
+ /**
78
+ * Stable device identifier — UUID persisted in `@capacitor/preferences`
79
+ * (native) or `localStorage` (web).
80
+ */
81
+ getId(): Promise<{
82
+ identifier: string;
83
+ }>;
84
+ /** Current locale language code, e.g. `'en'`, `'fr'`. */
85
+ getLanguageCode(): Promise<{
86
+ value: string;
87
+ }>;
88
+ }
89
+ /**
90
+ * Persistent key-value storage.
91
+ * Backed by `@capacitor/preferences` on native; `localStorage` on web;
92
+ * in-memory `Map` in private-browsing / third-party-cookie contexts.
93
+ */
94
+ export interface StorageFacade {
95
+ get(key: string): Promise<string | null>;
96
+ set(key: string, value: string): Promise<void>;
97
+ remove(key: string): Promise<void>;
98
+ keys(): Promise<string[]>;
99
+ }
100
+ /** QR / barcode scan. */
101
+ export interface QrScanOptions {
102
+ /** BarcodeFormat strings to restrict scanning (e.g. `['QR_CODE', 'EAN_13']`). */
103
+ formats?: string[];
104
+ }
105
+ export interface QrFacade {
106
+ /**
107
+ * Open the barcode scanner UI and resolve with the decoded string.
108
+ * Uses Capacitor MLKit on native; html5-qrcode (or `BarcodeDetector`) on web.
109
+ * Throws `HostTimeoutError` when the user dismisses without scanning.
110
+ */
111
+ scan(opts?: QrScanOptions): Promise<string>;
112
+ }
113
+ /** Authentication. */
114
+ export interface AuthFacade {
115
+ /**
116
+ * Initiate Google Sign-In. Returns the raw ID token for exchange with the
117
+ * SmartLinks auth backend. Falls back to an OAuth redirect flow on web.
118
+ */
119
+ signInWithGoogle(): Promise<{
120
+ idToken: string;
121
+ }>;
122
+ signOut(): Promise<void>;
123
+ }
124
+ /** NFC tag operations. */
125
+ export interface NfcReadResult {
126
+ uid: string;
127
+ /** Raw NDEF payload (hex or base64 depending on host), when present. */
128
+ ndef?: string;
129
+ }
130
+ export interface NfcFacade {
131
+ /**
132
+ * Wait for the next NFC tap and return the tag's UID + optional NDEF.
133
+ * Throws `HostTimeoutError` after `opts.timeoutMs` (default: 30 s).
134
+ */
135
+ read(opts?: {
136
+ timeoutMs?: number;
137
+ }): Promise<NfcReadResult>;
138
+ /** Write an NDEF record to a tag by UID. */
139
+ writeNdef(opts: {
140
+ uid: string;
141
+ payload: string;
142
+ }): Promise<void>;
143
+ /** Program an NTAG 21x / 424 profile. Host-specific; Kotlin only on advanced ops. */
144
+ programTag(opts: {
145
+ uid: string;
146
+ profile: string;
147
+ [key: string]: unknown;
148
+ }): Promise<void>;
149
+ /** Lock a tag against further writes. */
150
+ lockTag(opts: {
151
+ uid: string;
152
+ }): Promise<void>;
153
+ /** Returns whether the tag is currently locked. */
154
+ isLocked(opts: {
155
+ uid: string;
156
+ }): Promise<boolean>;
157
+ }
158
+ /** RFID mass-scan (UHF). Only available on `custom-android`. */
159
+ export interface RfidScanOptions {
160
+ /** Transmission power in dBm. */
161
+ power?: number;
162
+ /** Gen2 session flag (0–3). */
163
+ sessionFlag?: number;
164
+ }
165
+ export interface RfidFacade {
166
+ /** Start the RFID reader. Throws `HostCapabilityUnavailableError` on non-Kotlin hosts. */
167
+ startScan(opts?: RfidScanOptions): Promise<void>;
168
+ stopScan(): Promise<void>;
169
+ /** Subscribe to EPC bursts. Returns an unsubscribe function. */
170
+ subscribe(cb: (epcs: string[]) => void): () => void;
171
+ }
172
+ /** Cross-shell event bus. */
173
+ export interface EventsFacade {
174
+ /**
175
+ * Subscribe to named events from any of: the Kotlin bridge
176
+ * (`onSmartlinksData`), Capacitor plugin listeners, or internal pub/sub.
177
+ * Returns an unsubscribe function.
178
+ */
179
+ subscribe(type: string, cb: (payload: unknown) => void): () => void;
180
+ /** Publish an event into the same bus. Useful for container ↔ container comms. */
181
+ emit(type: string, payload?: unknown): void;
182
+ }
183
+ /** OTA / web-source switching. */
184
+ export type WebSourceMode = 'live' | 'bundled' | 'channel';
185
+ export interface WebSourceConfig {
186
+ mode: WebSourceMode;
187
+ /** Only set when `mode === 'channel'`. */
188
+ channel?: string;
189
+ /** Only set when `mode === 'live'`. */
190
+ liveUrl?: string;
191
+ }
192
+ export interface WebSourceFacade {
193
+ /** Returns the currently active web source config. */
194
+ get(): Promise<WebSourceConfig>;
195
+ /** Switch web source mode. Takes effect on next app boot. */
196
+ set(config: WebSourceConfig): Promise<void>;
197
+ }
198
+ /**
199
+ * The full native capability facade exposed by the host on
200
+ * `AdminMobileHostContext.native` (containers) and `window.SL.native` (UMD
201
+ * microapps).
202
+ *
203
+ * Each sub-facade is present only when the host implements it. Check
204
+ * `'nfc' in host.native` (or `host.capabilities` for hardware-gated features)
205
+ * before calling.
206
+ *
207
+ * @example
208
+ * const { native } = host;
209
+ * if (native?.qr) {
210
+ * const code = await native.qr.scan();
211
+ * }
212
+ */
213
+ export interface NativeFacade {
214
+ share: ShareFacade;
215
+ clipboard: ClipboardFacade;
216
+ haptics: HapticsFacade;
217
+ network: NetworkFacade;
218
+ device: DeviceFacade;
219
+ /** Cross-host persistent storage (`@capacitor/preferences` / `localStorage` fallback). */
220
+ storage: StorageFacade;
221
+ qr: QrFacade;
222
+ auth: AuthFacade;
223
+ nfc: NfcFacade;
224
+ /** RFID mass-scan. Only populated on `custom-android` hosts. */
225
+ rfid?: RfidFacade;
226
+ events: EventsFacade;
227
+ webSource: WebSourceFacade;
228
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Contract types for the `SL.native` / `host.native` facade.
3
+ *
4
+ * These are **interface-only** — no runtime implementation lives here.
5
+ * The facade is implemented per-host (Sidekick, Capacitor builds, PWA) and
6
+ * exposed to containers via `AdminMobileHostContext.native` or (for UMD
7
+ * microapps) via `window.SL.native`. The SDK owns the shape; the host owns
8
+ * the wiring.
9
+ *
10
+ * All facade methods reject with one of the structured errors from
11
+ * `@proveanything/smartlinks` (`HostCapabilityUnavailableError`,
12
+ * `HostPermissionDeniedError`, `HostTimeoutError`) so a single catch handler
13
+ * covers every host and capability.
14
+ */
15
+ export {};
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.11.2 | Generated: 2026-04-30T13:11:24.914Z
3
+ Version: 1.11.5 | Generated: 2026-04-30T16:31:53.616Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  This document describes how to build a **Mobile Admin Container** — a SmartLinks microapp that provides an in-the-field operator/admin surface optimised for mobile devices. These containers ship as a **separate `mobileAdmin` bundle** (not inside the `containers` bundle) so that Capacitor plugins, offline helpers, and operator-only code never reach the public consumer bundle.
6
6
 
7
- > **See also:** [containers.md](containers.md) covers the public consumer container. The [Multiple Consumer Components](containers.md#multiple-consumer-components) section explains the consumer vs. admin bundle split.
7
+ > **See also:** [containers.md](containers.md) covers the public consumer container. The [Multiple Consumer Components](containers.md#multiple-consumer-components) section explains the consumer vs. admin bundle split. For the full `host.native` facade contract (share, clipboard, haptics, NFC, RFID, storage, etc.) see [native-facade.md](native-facade.md).
8
8
 
9
9
  ---
10
10
 
@@ -23,6 +23,7 @@ This document describes how to build a **Mobile Admin Container** — a SmartLin
23
23
  11. [Build & Bundle Requirements](#build--bundle-requirements)
24
24
  12. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
25
25
  13. [Best Practices](#best-practices)
26
+ 14. [Native Capability Facade](#native-capability-facade)
26
27
 
27
28
  ---
28
29
 
@@ -58,7 +59,7 @@ Your container **never** detects the host directly. It receives a `host` prop fr
58
59
 
59
60
  Every container mounted by the SmartLinks Mobile launcher receives a single `host` prop — `AdminMobileHostContext`. Do not reach for `window.SmartlinksScanner` or `window.Capacitor` directly; both are wrapped here.
60
61
 
61
- > **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `ScannerEventSubscriber`, `MobileAdminComponentManifest`, and `MobileAdminBundleManifest` are all exported from `@proveanything/smartlinks`. Import via `import type { AdminMobileHostContext } from '@proveanything/smartlinks'` — no local mirror needed.
62
+ > **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `ActionableCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `AdminMobileEventCallback`, `AdminMobileEventSubscriber`, `AdminMobileComponentManifest`, `AdminMobileBundleManifest`, and `NativeFacade` (plus all sub-facade interfaces) are exported from `@proveanything/smartlinks`. Import via `import type { AdminMobileHostContext } from '@proveanything/smartlinks'` — no local mirror needed. `ScannerEventSubscriber`, `MobileAdminComponentManifest`, and `MobileAdminBundleManifest` still export as deprecated aliases.
62
63
 
63
64
  ```typescript
64
65
  interface AdminMobileHostContext {
@@ -82,8 +83,8 @@ interface AdminMobileHostContext {
82
83
  keyboard: boolean
83
84
  }
84
85
 
85
- // Unified hardware event stream
86
- events: { subscribe: ScannerEventSubscriber }
86
+ // Unified hardware event stream — callback type: AdminMobileEventCallback
87
+ events: { subscribe: (cb: AdminMobileEventCallback) => () => void }
87
88
 
88
89
  // Promise-based hardware actions — reject with a structured error when unavailable
89
90
  actions: {
@@ -97,12 +98,12 @@ interface AdminMobileHostContext {
97
98
  }
98
99
  }
99
100
 
100
- // Host-provided UI helpers
101
+ // Host-provided UI helpers — all optional, see §3 note
101
102
  ui: {
102
- toast: (opts: { title: string; description?: string; variant?: 'default' | 'destructive' }) => void
103
- haptic: (style?: 'light' | 'success' | 'error') => void
104
- setHeaderTitle: (title: string | null) => void
105
- navigateBack: () => void
103
+ toast?: (opts: { title: string; description?: string; variant?: 'default' | 'destructive' }) => void
104
+ haptic?: (style?: 'light' | 'success' | 'error') => void
105
+ setHeaderTitle?: (title: string | null) => void
106
+ navigateBack?: () => void
106
107
  }
107
108
 
108
109
  // Network & device info
@@ -112,6 +113,10 @@ interface AdminMobileHostContext {
112
113
  // Informational host version — use for logging/diagnostics, not feature detection.
113
114
  // For feature detection prefer existence checks: 'requestNfcTap' in host.actions
114
115
  _version: number
116
+
117
+ // Full native capability facade — share, clipboard, haptics, NFC, RFID, storage, etc.
118
+ // Optional: not every host stub populates it. See native-facade.md.
119
+ native?: NativeFacade
115
120
  }
116
121
  ```
117
122
 
@@ -143,9 +148,10 @@ if (host.hardware.nfc) {
143
148
 
144
149
  ### `host.ui` — native helpers vs. your own components
145
150
 
146
- `host.ui.setHeaderTitle()` and `host.ui.navigateBack()` are **host-only**there is no in-container equivalent. Call them via `host.ui` or omit them.
151
+ All four `host.ui` methods are **optional by design**. A container may run in the Sidekick mobile shell, a standalone PWA or browser tab, Storybook, or a screenshot harness none of those environments is guaranteed to have a native toast system, a managed header, or a back stack. Forcing every host implementer to provide them would exclude the web and desktop use-cases the rest of the contract explicitly supports. Always guard with `?.`:
147
152
 
148
- `host.ui.toast()` and `host.ui.haptic()` are **optional conveniences**. Use them when you want native system feedback. When rendering in Storybook, unit tests, or a plain browser tab, your own `<Toaster />` is a perfectly valid substitute — you do not need to stub the entire `host.ui` surface just to get toast notifications.
153
+ - `host.ui.toast?.({...})` / `host.ui.haptic?.('success')` optional native feedback. Fall back to your own `<Toaster />` when absent.
154
+ - `host.ui.setHeaderTitle?.('Scanning…')` / `host.ui.navigateBack?.()` — host-shell integrations with no in-container equivalent. Silently no-op when the host doesn't implement them.
149
155
 
150
156
  **Stub pattern for testing and Storybook:**
151
157
 
@@ -222,12 +228,23 @@ The custom Kotlin shell and both Capacitor shells ship the same baseline plugin
222
228
  | `@capacitor/device` | `host.device.info()` |
223
229
  | `@capacitor/share` | `host.actions.share()` |
224
230
  | `@capacitor/clipboard` | `host.actions.clipboard.*` |
225
- | `@capacitor/preferences` | `host.storage.*` *(planned)* |
231
+ | `@capacitor/preferences` | `host.storage.*` *(planned — see note below)* |
226
232
  | `@capacitor/app` | host-managed (back button, deep links) |
227
233
  | `@capacitor/status-bar` | host-managed |
228
234
  | `@capacitor/keyboard` | host-managed |
229
235
  | `@capacitor/toast` | wired into `host.ui.toast()` |
230
236
 
237
+ > **`host.storage` — planned shape.** Once released, the surface will wrap `@capacitor/preferences` directly:
238
+ > ```ts
239
+ > host.storage: {
240
+ > get(key: string): Promise<string | null>
241
+ > set(key: string, value: string): Promise<void>
242
+ > remove(key: string): Promise<void>
243
+ > keys(): Promise<string[]>
244
+ > }
245
+ > ```
246
+ > Until then: `localStorage` works on web hosts; use `@capacitor/preferences` directly (bundle it in) on native.
247
+
231
248
  ### Tier 2 — capability-gated (declare in manifest)
232
249
 
233
250
  | Plugin | Capability flag |
@@ -385,7 +402,9 @@ try {
385
402
  | `lifecycle: 'pause'` | App backgrounded | Pause readers to save battery |
386
403
  | `lifecycle: 'resume'` | App foregrounded | Resubscribe; refresh stale data |
387
404
 
388
- Use `host.events.subscribe` for all lifecycle events — it fires identically on every host.
405
+ Use `host.events.subscribe` for all lifecycle events — all five host types (`custom-android`, `capacitor-ios`, `capacitor-android`, `pwa`, `browser`) are guaranteed to emit every `'pause'`/`'resume'`/`'offline'`/`'online'` phase. No `window.addEventListener('online')` fallback is needed.
406
+
407
+ > **Planned** — `phase: 'mount' | 'unmount'` events are on the roadmap. These will fire when the container becomes visible / is removed from the host view stack, enabling deferred reader start-up without a `useEffect` dependency.
389
408
 
390
409
  ---
391
410
 
@@ -521,10 +540,33 @@ export const MOBILE_ADMIN_MANIFEST = {
521
540
 
522
541
  - **Always check `host.hardware.X` before calling `host.actions.X`** — never assume a capability is available.
523
542
  - **Always wrap action calls in try/catch** — handle `HostPermissionDeniedError`, `HostTimeoutError`, and `HostCapabilityUnavailableError`.
524
- - **Use `host.ui.setHeaderTitle` and `host.ui.navigateBack`** for header integrationthese are host-only and have no in-container equivalent.
525
- - **`host.ui.toast` and `host.ui.haptic` are optional** use them for native feedback, or fall back to your own `<Toaster />` when testing in isolation.
543
+ - **All four `host.ui` methods are optional** (`toast`, `haptic`, `setHeaderTitle`, `navigateBack`)guard every call with `?.`. See the `host.ui` section above.
544
+ - **`host.ui.setHeaderTitle` and `host.ui.navigateBack`** integrate with the host shell and have no in-container equivalent; call with `?.`.
526
545
  - **Use `host.events.subscribe` for lifecycle events** — `'offline'`/`'online'`/`'pause'`/`'resume'` fire consistently on every host.
527
546
  - **Never call `initializeApi`, never use top-level SDK imports for API calls** — `host.SL` is already configured. `SL.method()` instead of `host.SL.method()` silently uses the wrong baseURL.
528
547
  - **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
529
548
  - **Declare `offline: true`** on a component if it queues writes locally — this signals the launcher to provision offline sync support.
530
549
  - **Use `'methodName' in host.actions`** to feature-detect new host capabilities rather than comparing `host._version`. The version is informational only.
550
+
551
+ ---
552
+
553
+ ## Native Capability Facade
554
+
555
+ `host.native` gives containers access to a broader set of device capabilities — share sheet, clipboard, full haptics API, storage, QR, NFC, RFID (Kotlin only), auth, and cross-shell events — through a single interface that falls back gracefully across all host environments.
556
+
557
+ ```typescript
558
+ // Check host.hardware.* for physical availability, then call via host.native
559
+ if (host.hardware.nfc && host.native?.nfc) {
560
+ const { uid } = await host.native.nfc.read({ timeoutMs: 10_000 })
561
+ }
562
+
563
+ // Storage always available (Preferences → localStorage → in-memory Map)
564
+ await host.native?.storage.set('lastScan', uid)
565
+
566
+ // Share sheet with web fallback
567
+ await host.native?.share.share({ title: 'Found tag', url: `https://app.example/tags/${uid}` })
568
+ ```
569
+
570
+ `host.native` is optional on `AdminMobileHostContext` — host stubs (Storybook, unit tests) need not implement it. `native.rfid` is additionally optional within `NativeFacade` itself, as it only exists on `custom-android`.
571
+
572
+ For the full sub-facade table, fallback chains, and what's intentionally not wrapped, see **[native-facade.md](native-facade.md)**.
@@ -0,0 +1,170 @@
1
+ # Native Capability Facade (`host.native` / `SL.native`)
2
+
3
+ > **Version:** 1.12 · **Platform:** SmartLinks R4 · **Last updated:** 2026-04-30
4
+
5
+ The `NativeFacade` is a thin contract layer between microapps and the device capabilities available on the current host shell (Kotlin, Capacitor iOS/Android, PWA, or browser). It lets a microapp call `host.native.share.share({...})` without knowing whether it's running over `window.SmartlinksScanner`, a Capacitor plugin, or `navigator.share`.
6
+
7
+ **The SDK owns the contract** (`src/native/types.ts`, re-exported from `@proveanything/smartlinks`). Each host implementation is responsible for wiring up the sub-facades and populating:
8
+ - `AdminMobileHostContext.native` (container prop, typed in the SDK), and
9
+ - `window.SL.native` (for UMD microapps that don't receive a host prop — host concern, not SDK).
10
+
11
+ ---
12
+
13
+ ## Table of Contents
14
+
15
+ 1. [Access patterns](#access-patterns)
16
+ 2. [What's in the facade](#whats-in-the-facade)
17
+ 3. [What's NOT in the facade](#whats-not-in-the-facade)
18
+ 4. [Error contract](#error-contract)
19
+ 5. [Type reference](#type-reference)
20
+
21
+ ---
22
+
23
+ ## Access patterns
24
+
25
+ ### Inside a Mobile Admin Container (recommended)
26
+
27
+ ```typescript
28
+ import type { AdminMobileHostContext } from '@proveanything/smartlinks'
29
+
30
+ function MyContainer({ host }: { host: AdminMobileHostContext }) {
31
+ const handleShare = async () => {
32
+ await host.native?.share.share({ title: 'SmartLinks', url: window.location.href })
33
+ }
34
+
35
+ const handleScan = async () => {
36
+ // Only present when the host provides a QR reader
37
+ const code = await host.native?.qr.scan()
38
+ if (code) processCode(code)
39
+ }
40
+ }
41
+ ```
42
+
43
+ Always call via `host.native?.<facade>.<method>()` — `native` is optional because not every host exposes the full surface (e.g. a minimal browser stub may omit `rfid`).
44
+
45
+ For hardware-gated capabilities (NFC, RFID, QR, camera) also check `host.capabilities` or `host.hardware.*` before calling, as those flags reflect physical availability on the current device:
46
+
47
+ ```typescript
48
+ if (host.hardware.nfc && host.native?.nfc) {
49
+ const { uid } = await host.native.nfc.read({ timeoutMs: 10_000 })
50
+ }
51
+ ```
52
+
53
+ ### Inside a UMD microapp (no host prop)
54
+
55
+ ```typescript
56
+ // window.SL.native is populated by the host at boot — host concern, not SDK
57
+ const native = (window as any).SL?.native as import('@proveanything/smartlinks').NativeFacade | undefined
58
+ const code = await native?.qr.scan()
59
+ ```
60
+
61
+ ---
62
+
63
+ ## What's in the facade
64
+
65
+ These capabilities have meaningful fallbacks across Kotlin / Capacitor / web. Always go through the facade.
66
+
67
+ | Sub-facade | Key methods | Fallback chain |
68
+ |---|---|---|
69
+ | `native.share` | `share({ title, url, text? })`, `canShare()` | Capacitor Share → `navigator.share` → copy to clipboard + toast |
70
+ | `native.clipboard` | `write(text)`, `read()` | Capacitor Clipboard → `navigator.clipboard` → `execCommand('copy')` |
71
+ | `native.haptics` | `impact(style?)`, `notification(style)`, `selection()` | Capacitor Haptics → `navigator.vibrate` → no-op |
72
+ | `native.network` | `getStatus()`, `addListener('change', cb)` | Capacitor Network → `navigator.onLine` + `'online'`/`'offline'` events |
73
+ | `native.device` | `getInfo()`, `getId()`, `getLanguageCode()` | Capacitor Device → UA parsing + cached UUID in `localStorage` |
74
+ | `native.storage` | `get(key)`, `set(key, value)`, `remove(key)`, `keys()` | Capacitor Preferences → `localStorage` → in-memory `Map` (private mode) |
75
+ | `native.qr` | `scan(opts?)` | Capacitor MLKit Barcode → html5-qrcode / `BarcodeDetector` |
76
+ | `native.auth` | `signInWithGoogle()`, `signOut()` | Kotlin bridge → Capacitor Browser + OAuth → web redirect |
77
+ | `native.nfc` | `read(opts?)`, `writeNdef(opts)`, `programTag(opts)`, `lockTag(opts)`, `isLocked(opts)` | Kotlin bridge → Web NFC (`NDEFReader`) → `HostCapabilityUnavailableError` |
78
+ | `native.rfid` | `startScan(opts?)`, `stopScan()`, `subscribe(cb)` | Kotlin bridge only — throws `HostCapabilityUnavailableError` on all other hosts |
79
+ | `native.events` | `subscribe(type, cb)`, `emit(type, payload?)` | Internal pub/sub bridging Kotlin `onSmartlinksData` + Capacitor listeners |
80
+ | `native.webSource` | `get()`, `set({ mode, channel?, liveUrl? })` | Kotlin bridge + Capacitor Preferences mirror |
81
+
82
+ > **`native.rfid` is the only optional sub-facade** on `NativeFacade` — it is only populated on `custom-android` hosts. All other sub-facades are present on every host, though individual methods may throw `HostCapabilityUnavailableError` when the underlying capability is absent.
83
+
84
+ > **`native.device` vs `host.device`** — `host.device.info()` is a simplified convenience that returns `{ model, platform }`. `host.native?.device.getInfo()` returns the full `DeviceInfo` (adds `osVersion`, `manufacturer`, `isVirtual`) and also provides `getId()` and `getLanguageCode()`. Use `host.device` when the convenience is enough.
85
+
86
+ > **`native.network` vs `host.network`** — `host.network.isOnline()` is a boolean convenience. `host.native?.network.getStatus()` returns `{ connected, connectionType }` including `'wifi'` / `'cellular'` distinction.
87
+
88
+ ---
89
+
90
+ ## What's NOT in the facade
91
+
92
+ These capabilities intentionally have no `SL.native` wrapper. Call the Capacitor plugin directly, or use the web API. The host provides them as Tier 1 or Tier 2 plugins (see [Capacitor Plugin Baseline](mobile-admin-container.md#capacitor-plugin-baseline)).
93
+
94
+ | Capability | Why not wrapped |
95
+ |---|---|
96
+ | `@capacitor/push-notifications` | Push only exists in Capacitor builds; no cross-shell registration flow. Import and call directly; detect Capacitor first. |
97
+ | `@capacitor/local-notifications` | Same — no Kotlin or web equivalent worth abstracting. |
98
+ | `@capacitor/camera` | Plugin already abstracts iOS/Android. PWA's `<input capture>` is too different in shape to unify. |
99
+ | `@capacitor/filesystem` | No web equivalent (storage quotas, sandbox rules differ). Use `URL.createObjectURL` + download anchor on web. |
100
+ | `@capacitor/geolocation` | `navigator.geolocation` already works in WebViews; the Capacitor plugin wraps the same OS APIs. No facade adds value. |
101
+ | `@capacitor/keyboard`, `@capacitor/screen-*`, `@capacitor/splash-screen`, `@capacitor/status-bar`, `@capacitor/dialog`, `@capacitor/app` | Per-host boot-time config or platform-chrome concerns; not part of the microapp contract. |
102
+ | `@capacitor/browser` | In-app browser for OAuth is intentionally Capacitor-only. For opening external URLs use `native.share.share({ url })`. |
103
+ | `@capawesome/capacitor-file-picker` | Picker UX too divergent across web (`<input type="file">`) and native to wrap usefully. |
104
+
105
+ If a capability later needs consistent behaviour across shells (e.g. notifications reach enough hosts to warrant throttling / routing via the facade), promote it into `NativeFacade` by adding an interface to `src/native/types.ts` and a row to the table above.
106
+
107
+ ---
108
+
109
+ ## Error contract
110
+
111
+ All facade methods reject with one of the three structured errors from `@proveanything/smartlinks`. A single catch block handles every capability and host combination:
112
+
113
+ ```typescript
114
+ import {
115
+ HostCapabilityUnavailableError,
116
+ HostPermissionDeniedError,
117
+ HostTimeoutError,
118
+ } from '@proveanything/smartlinks'
119
+
120
+ try {
121
+ const { uid } = await host.native?.nfc.read({ timeoutMs: 10_000 }) ?? {}
122
+ } catch (err) {
123
+ if (err instanceof HostCapabilityUnavailableError) {
124
+ // host.capabilities won't list 'nfc' — we should have checked first
125
+ host.ui.toast?.({ title: 'NFC not available on this device', variant: 'destructive' })
126
+ } else if (err instanceof HostPermissionDeniedError) {
127
+ host.ui.toast?.({ title: 'NFC permission denied', variant: 'destructive' })
128
+ } else if (err instanceof HostTimeoutError) {
129
+ host.ui.toast?.({ title: `No tag detected after ${err.timeoutMs / 1000}s` })
130
+ } else {
131
+ throw err
132
+ }
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Type reference
139
+
140
+ All types below are exported from `@proveanything/smartlinks`:
141
+
142
+ ```typescript
143
+ import type {
144
+ NativeFacade, // root — use on AdminMobileHostContext.native
145
+ NativeCapability, // union of sub-facade keys: 'share' | 'clipboard' | ...
146
+ ShareFacade,
147
+ ClipboardFacade,
148
+ HapticsFacade,
149
+ HapticImpactStyle, // 'light' | 'medium' | 'heavy'
150
+ HapticNotificationStyle,
151
+ NetworkFacade,
152
+ NetworkStatus,
153
+ DeviceFacade,
154
+ DeviceInfo,
155
+ StorageFacade,
156
+ QrFacade,
157
+ QrScanOptions,
158
+ AuthFacade,
159
+ NfcFacade,
160
+ NfcReadResult,
161
+ RfidFacade,
162
+ RfidScanOptions,
163
+ EventsFacade,
164
+ WebSourceFacade,
165
+ WebSourceMode,
166
+ WebSourceConfig,
167
+ } from '@proveanything/smartlinks'
168
+ ```
169
+
170
+ The facade interfaces are contract-only — no runtime implementation is shipped in the SDK. You only need them for TypeScript type-checking (authoring containers or host stubs).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.11.2",
3
+ "version": "1.11.5",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",