@proveanything/smartlinks 1.10.2 → 1.11.0

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.10.2 | Generated: 2026-04-27T09:32:45.351Z
3
+ Version: 1.11.0 | Generated: 2026-04-30T11:35:44.821Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -91,6 +91,24 @@ The manifest is loaded automatically by the platform for every collection page.
91
91
  ]
92
92
  },
93
93
 
94
+ "mobileAdmin": {
95
+ "files": {
96
+ "js": {
97
+ "umd": "dist/mobile-admin.umd.js",
98
+ "esm": "dist/mobile-admin.es.js"
99
+ },
100
+ "css": null
101
+ },
102
+ "components": [
103
+ {
104
+ "name": "WarehousePickContainer",
105
+ "description": "In-field operator admin surface.",
106
+ "audience": "admin-mobile",
107
+ "capabilities": ["nfc", "qr", "offline-queue"]
108
+ }
109
+ ]
110
+ },
111
+
94
112
  "linkable": [
95
113
  { "title": "Home", "path": "/" },
96
114
  { "title": "Gallery", "path": "/gallery" },
@@ -188,7 +206,52 @@ This tells the platform and other apps that they can deep-link into a stored wid
188
206
  Same structure as `widgets` but declares the full-app container bundle. Lazy-loaded on demand.
189
207
 
190
208
  See the [Containers guide](containers.md) for details on the container component model.
209
+ **Component fields** (same as widgets, plus):
210
+
211
+ | Field | Type | Description |
212
+ |-------|------|--------------|
213
+ | `name` | string | Exported component name |
214
+ | `description` | string | Human-readable description |
215
+ | `props.required` / `props.optional` | string[] | Required and optional prop names |
216
+ | `audience` | `"public"` \| `"admin"` \| `"both"` | Who can use/see this component. Defaults to `"public"`. |
217
+ | `scope` | `"collection"` \| `"product"` | Data scope hint. `"product"` means the component always renders in the context of a specific product. |
218
+ | `settings` | object | JSON Schema describing configurable settings |
191
219
 
220
+ #### `mobileAdmin`
221
+
222
+ Declares a **separate** mobile admin bundle — a sibling of `containers` with its own build output. Use this when the mobile admin surface needs a different runtime, native-only dependencies (Capacitor), or independent versioning. Omit if your app has no mobile admin surface.
223
+
224
+ See [mobile-admin-container.md](mobile-admin-container.md) for the `AdminMobileHostContext` prop contract, the capability matrix, event stream, error types, and build setup.
225
+
226
+ ```json
227
+ "mobileAdmin": {
228
+ "files": {
229
+ "js": {
230
+ "umd": "dist/mobile-admin.umd.js",
231
+ "esm": "dist/mobile-admin.es.js"
232
+ },
233
+ "css": null
234
+ },
235
+ "components": [
236
+ {
237
+ "name": "WarehousePickContainer",
238
+ "description": "Pick orders by scanning NFC tags",
239
+ "audience": "admin-mobile",
240
+ "capabilities": ["nfc", "qr", "offline-queue"]
241
+ }
242
+ ]
243
+ }
244
+ ```
245
+
246
+ | Field | Description |
247
+ |-------|-------------|
248
+ | `files.js.umd` | UMD bundle path — used for dynamic `<script>` loading |
249
+ | `files.js.esm` | ESM bundle path (optional but recommended) |
250
+ | `files.css` | CSS bundle path — set to `null` if no styles |
251
+ | `components[].name` | Exported component name (must match the UMD bundle export) |
252
+ | `components[].description` | Shown in the mobile launcher's app picker |
253
+ | `components[].audience` | Must be `"admin-mobile"` |
254
+ | `components[].capabilities` | Hardware/feature capabilities this component needs or can use. See [capability list](mobile-admin-container.md#hardware-capabilities--the-capability-matrix). |
192
255
  #### `linkable`
193
256
 
194
257
  Static deep-linkable states built into the app — fixed routes that exist regardless of per-collection content. Declared once at build time.
@@ -446,3 +446,26 @@ src/containers/
446
446
  | Query cache conflicts | Sharing parent's QueryClient | Each container needs its own `QueryClient` instance |
447
447
  | `cva.cva` runtime error | Global set to lowercase `cva` | Use uppercase `CVA` for the global name |
448
448
  | Navigation does nothing | Using legacy string with `onNavigate` | Use structured `NavigationRequest` object instead |
449
+
450
+ ---
451
+
452
+ ## Multiple Consumer Components
453
+
454
+ A single `containers` bundle can export more than one component — useful when an app wants to offer different UX styles for different portal contexts (e.g. a portal card view vs a hub embed view). Each is a named export in the same bundle, declared as a separate entry in `components[]`:
455
+
456
+ ```json
457
+ "containers": {
458
+ "components": [
459
+ {
460
+ "name": "PortalContainer",
461
+ "description": "Full consumer experience for the SmartLinks portal"
462
+ },
463
+ {
464
+ "name": "HubContainer",
465
+ "description": "Compact consumer embed for hub-style layouts"
466
+ }
467
+ ]
468
+ }
469
+ ```
470
+
471
+ All components in `containers` are consumer-facing. **Admin and operator surfaces always ship as a separate bundle** (`mobileAdmin` or via iframe). See [mobile-admin-container.md](mobile-admin-container.md) for the separate-bundle approach.
@@ -0,0 +1,467 @@
1
+ # Mobile Admin Container SDK
2
+
3
+ > **Version:** 1.0 · **Platform:** SmartLinks R4 · **Last updated:** 2026-04-30
4
+
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
+
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.
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ 1. [When to Build a Mobile Admin Container](#when-to-build-a-mobile-admin-container)
14
+ 2. [Host Environment](#host-environment)
15
+ 3. [The `host` Prop](#the-host-prop)
16
+ 4. [Hardware Capabilities & the Capability Matrix](#hardware-capabilities--the-capability-matrix)
17
+ 5. [Capacitor Plugin Baseline](#capacitor-plugin-baseline)
18
+ 6. [Manifest Declaration](#manifest-declaration)
19
+ 7. [Event Stream](#event-stream)
20
+ 8. [Error Handling](#error-handling)
21
+ 9. [Lifecycle](#lifecycle)
22
+ 10. [Build & Bundle Requirements](#build--bundle-requirements)
23
+ 11. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
24
+ 12. [Best Practices](#best-practices)
25
+
26
+ ---
27
+
28
+ ## When to Build a Mobile Admin Container
29
+
30
+ Build a Mobile Admin Container when the surface is intended for **field operators or admins** rather than consumers — especially when:
31
+
32
+ - You need native hardware: NFC, RFID, camera/barcode, haptics, geolocation
33
+ - The surface must work offline or in flaky network conditions
34
+ - You want the bundle to version independently from the public consumer app
35
+
36
+ All admin-mobile containers ship in the `mobileAdmin` bundle and are never loaded by the public portal. The mobile launcher loads them via the `mobileAdmin` manifest key.
37
+
38
+ ---
39
+
40
+ ## Host Environment
41
+
42
+ The SmartLinks Mobile launcher runs on five host environments:
43
+
44
+ | Host ID | Description | Hardware bridge |
45
+ |---------|-------------|-----------------|
46
+ | `custom-android` | Original Kotlin WebView app — full RFID + NTAG-advanced | `window.SmartlinksScanner` |
47
+ | `capacitor-ios` | iOS app built via Capacitor | Capacitor plugins |
48
+ | `capacitor-android` | Generic Android Capacitor build (no RFID) | Capacitor plugins |
49
+ | `pwa` | Installed Progressive Web App | Web APIs only |
50
+ | `browser` | Regular browser tab | Web APIs only |
51
+
52
+ Your container **never** detects the host directly. It receives a `host` prop from the launcher and reads `host.hardware.*` capability flags. The launcher handles the rest.
53
+
54
+ ---
55
+
56
+ ## The `host` Prop
57
+
58
+ 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.
59
+
60
+ ```typescript
61
+ interface AdminMobileHostContext {
62
+ // Identity
63
+ collectionId: string
64
+ appId: string
65
+ user: { uid?: string; email?: string; displayName?: string; isAdmin: boolean } | null
66
+
67
+ // SDK — already initialised. Do NOT call initializeApi again.
68
+ SL: typeof import('@proveanything/smartlinks')
69
+
70
+ // Capabilities declared by this container in its manifest
71
+ capabilities: AdminMobileCapability[]
72
+
73
+ // Live capability flags for the current device
74
+ hardware: {
75
+ nfc: boolean
76
+ rfid: boolean
77
+ qr: boolean
78
+ camera: boolean
79
+ keyboard: boolean
80
+ }
81
+
82
+ // Unified hardware event stream
83
+ events: { subscribe: ScannerEventSubscriber }
84
+
85
+ // Promise-based hardware actions — reject with a structured error when unavailable
86
+ actions: {
87
+ requestQrScan: () => Promise<string>
88
+ requestNfcTap: (timeoutMs?: number) => Promise<{ uid: string; ndef?: string }>
89
+ requestCameraPhoto: () => Promise<Blob>
90
+ share: (payload: { title: string; url: string; text?: string }) => Promise<void>
91
+ clipboard: {
92
+ read: () => Promise<string>
93
+ write: (text: string) => Promise<void>
94
+ }
95
+ }
96
+
97
+ // Host-provided UI helpers
98
+ ui: {
99
+ toast: (opts: { title: string; description?: string; variant?: 'default' | 'destructive' }) => void
100
+ haptic: (style?: 'light' | 'success' | 'error') => void
101
+ setHeaderTitle: (title: string | null) => void
102
+ navigateBack: () => void
103
+ }
104
+
105
+ // Network & device info
106
+ network: { isOnline: () => boolean }
107
+ device: { info: () => Promise<{ model: string; platform: string }> }
108
+
109
+ // Host context version — for future feature-detection
110
+ _version: number
111
+ }
112
+ ```
113
+
114
+ ### Contract rules
115
+
116
+ 1. **Check `host.hardware.X` before calling `host.actions.X`.** Calls to unavailable capabilities reject with `HostCapabilityUnavailableError`.
117
+ 2. **Do not call `initializeApi`.** `host.SL` is already configured with the right `baseURL`, auth, and logger.
118
+ 3. **Do not access `window.SmartlinksScanner` or `window.Capacitor` directly.** The host wraps both; direct access produces containers that only work on one shell.
119
+ 4. **Externalise all shared deps** (React, ReactDOM, SL, Radix, etc.) — the launcher provides them as globals. See [Build & Bundle Requirements](#build--bundle-requirements).
120
+
121
+ ```typescript
122
+ // Example — always check hardware flags first
123
+ const { host } = props
124
+
125
+ if (host.hardware.nfc) {
126
+ host.ui.setHeaderTitle('Scan NFC tag')
127
+ const { uid } = await host.actions.requestNfcTap(5000)
128
+ // ...
129
+ }
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Hardware Capabilities & the Capability Matrix
135
+
136
+ Containers declare which capabilities they need (or can use) in the manifest `capabilities` array. The launcher uses this list to:
137
+
138
+ 1. **Filter availability** — hide containers whose required hardware is not on this device
139
+ 2. **Pre-warm** — start the right reader as soon as the container mounts
140
+ 3. **Permission prompts** — request only the permissions the container actually needs
141
+
142
+ | Capability | Description |
143
+ |------------|-------------|
144
+ | `"nfc"` | NFC tap — UID + NDEF read |
145
+ | `"nfc-advanced"` | NTAG 21x mirror config, NTAG 424 SUN, key rotation (`custom-android` only) |
146
+ | `"rfid"` | UHF RFID mass scan (`custom-android` only) |
147
+ | `"qr"` | QR / barcode scanning (camera-based) |
148
+ | `"camera"` | Photo capture, gallery picker |
149
+ | `"keyboard"` | Physical hardware trigger/action buttons |
150
+ | `"geolocation"` | GPS coordinates |
151
+ | `"offline-queue"` | Container needs local write queuing + sync |
152
+ | `"push"` | Remote push notifications (FCM/APNs) |
153
+
154
+ The full hardware capability matrix by host:
155
+
156
+ | Capability | custom-android | capacitor-ios | capacitor-android | pwa | browser |
157
+ |------------|:-:|:-:|:-:|:-:|:-:|
158
+ | NFC tap (UID + NDEF read) | ✅ | ✅ | ✅ | ⚠️ Chrome Android | ⚠️ |
159
+ | NFC NDEF write | ✅ | ❌ | ✅ | ⚠️ | ⚠️ |
160
+ | NFC advanced (NTAG 21x / 424) | ✅ | ❌ | ❌ | ❌ | ❌ |
161
+ | RFID mass scan | ✅ | ❌ | ❌ | ❌ | ❌ |
162
+ | QR / barcode scan | ✅ | ✅ | ✅ | ✅ | ✅ |
163
+ | Camera photo | ✅ | ✅ | ✅ | ✅ | ✅ |
164
+ | Hardware key events | ✅ | ❌ | ❌ | ❌ | ❌ |
165
+ | Haptics | ✅ | ✅ | ✅ | ⚠️ visual only | ⚠️ |
166
+
167
+ > Containers that declare `"rfid"` or `"nfc-advanced"` are only offered on `custom-android`. All others are assumed to run on all five hosts — design for the lowest common denominator and use `host.hardware.*` flags to unlock richer UI on capable devices.
168
+
169
+ ---
170
+
171
+ ## Capacitor Plugin Baseline
172
+
173
+ The custom Kotlin shell and both Capacitor shells ship the same baseline plugin set. Container authors can rely on all Tier 1 plugins on every native host.
174
+
175
+ ### Tier 1 — always available on every native host
176
+
177
+ | Plugin | Surfaced via |
178
+ |--------|-------------|
179
+ | `@capacitor/haptics` | `host.ui.haptic()` |
180
+ | `@capacitor/network` | `host.network.isOnline()` |
181
+ | `@capacitor/device` | `host.device.info()` |
182
+ | `@capacitor/share` | `host.actions.share()` |
183
+ | `@capacitor/clipboard` | `host.actions.clipboard.*` |
184
+ | `@capacitor/preferences` | `host.storage.*` *(planned)* |
185
+ | `@capacitor/app` | host-managed (back button, deep links) |
186
+ | `@capacitor/status-bar` | host-managed |
187
+ | `@capacitor/keyboard` | host-managed |
188
+ | `@capacitor/toast` | wired into `host.ui.toast()` |
189
+
190
+ ### Tier 2 — capability-gated (declare in manifest)
191
+
192
+ | Plugin | Capability flag |
193
+ |--------|----------------|
194
+ | `@capacitor/camera` | `"camera"` |
195
+ | `@capacitor-mlkit/barcode-scanning` | `"qr"` |
196
+ | `@capgo/capacitor-nfc` | `"nfc"` |
197
+ | `@capacitor/geolocation` | `"geolocation"` |
198
+ | `@capacitor/push-notifications` | `"push"` |
199
+ | `@capacitor/filesystem` | *(declare `"offline-queue"` if needed)* |
200
+
201
+ ### Tier 3 — custom-android only (not Capacitor)
202
+
203
+ Proprietary hardware SDKs (UHF RFID, NTAG 21x/424 advanced programming, USB uFR reader feedback). These are wrapped behind `host.actions.*` — never access `window.SmartlinksScanner` directly.
204
+
205
+ ---
206
+
207
+ ## Manifest Declaration
208
+
209
+ Declare the bundle under the top-level `mobileAdmin` key in `app.manifest.json`. This is separate from `containers` — the mobile launcher reads `mobileAdmin`; the public portal never loads it.
210
+
211
+ ```json
212
+ {
213
+ "meta": { "appId": "my-app", "name": "My App", "version": "1.0.0" },
214
+
215
+ "containers": {
216
+ "files": { "js": { "umd": "dist/containers.umd.js", "esm": "dist/containers.es.js" }, "css": "dist/containers.css" },
217
+ "components": [
218
+ { "name": "PublicContainer", "description": "Default consumer experience" }
219
+ ]
220
+ },
221
+
222
+ "mobileAdmin": {
223
+ "files": {
224
+ "js": {
225
+ "umd": "dist/mobile-admin.umd.js",
226
+ "esm": "dist/mobile-admin.es.js"
227
+ },
228
+ "css": null
229
+ },
230
+ "components": [
231
+ {
232
+ "name": "WarehousePickContainer",
233
+ "description": "Pick orders by scanning NFC tags",
234
+ "audience": "admin-mobile",
235
+ "capabilities": ["nfc", "qr", "offline-queue"]
236
+ }
237
+ ]
238
+ }
239
+ }
240
+ ```
241
+
242
+ A `mobileAdmin` bundle can export multiple components targeting different operator workflows — each with its own `capabilities` declaration.
243
+
244
+ **Component fields:**
245
+
246
+ | Field | Type | Description |
247
+ |-------|------|-------------|
248
+ | `name` | string | Exported component name (must match the UMD bundle export) |
249
+ | `description` | string | Shown in the mobile launcher's app picker |
250
+ | `audience` | `"admin-mobile"` | Required — tells the launcher this is an operator surface |
251
+ | `capabilities` | string[] | Capabilities this component needs or can use. See table above. |
252
+
253
+ ---
254
+
255
+ ## Event Stream
256
+
257
+ 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.
258
+
259
+ ```typescript
260
+ useEffect(() => {
261
+ const unsubscribe = host.events.subscribe((event) => {
262
+ switch (event.type) {
263
+ case 'nfc-tap': handleNfc(event.uid, event.ndef); break
264
+ case 'rfid-burst': handleEpcs(event.epcs); break
265
+ case 'qr-scan': handleQr(event.code); break
266
+ case 'key-press': if (event.keyCode === 293) startScan(); break
267
+ case 'lifecycle': handleLifecycle(event.phase); break
268
+ }
269
+ })
270
+ return unsubscribe
271
+ }, [host.events])
272
+ ```
273
+
274
+ 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.
275
+
276
+ ---
277
+
278
+ ## Error Handling
279
+
280
+ Action methods reject with structured errors — wrap every `host.actions.*` call even on hosts where the capability "should" be available (the user can deny permission at runtime):
281
+
282
+ ```typescript
283
+ class HostCapabilityUnavailableError extends Error {
284
+ capability: 'nfc' | 'rfid' | 'qr' | 'camera'
285
+ host: string // host ID
286
+ }
287
+
288
+ class HostPermissionDeniedError extends Error {
289
+ capability: 'nfc' | 'rfid' | 'qr' | 'camera'
290
+ }
291
+
292
+ class HostTimeoutError extends Error {
293
+ capability: 'nfc' | 'qr'
294
+ timeoutMs: number
295
+ }
296
+ ```
297
+
298
+ ```typescript
299
+ try {
300
+ const code = await host.actions.requestQrScan()
301
+ host.ui.haptic('success')
302
+ } catch (err) {
303
+ if (err instanceof HostPermissionDeniedError) {
304
+ host.ui.toast({ title: 'Camera access required', variant: 'destructive' })
305
+ } else if (err instanceof HostTimeoutError) {
306
+ host.ui.toast({ title: 'No QR code detected' })
307
+ } else {
308
+ throw err
309
+ }
310
+ }
311
+ ```
312
+
313
+ ---
314
+
315
+ ## Lifecycle
316
+
317
+ | Event | When | What to do |
318
+ |-------|------|------------|
319
+ | mount | User opens your container | Subscribe to events, start readers |
320
+ | unmount | User backs out / app backgrounded > 30s | Unsubscribe; persist in-flight state |
321
+ | `lifecycle: 'offline'` | Network lost | Switch to offline-queue mode |
322
+ | `lifecycle: 'online'` | Network restored | Flush queued writes |
323
+ | `lifecycle: 'pause'` | App backgrounded | Pause readers to save battery |
324
+ | `lifecycle: 'resume'` | App foregrounded | Resubscribe; refresh stale data |
325
+
326
+ Use `host.events.subscribe` for all lifecycle events — it fires identically on every host.
327
+
328
+ ---
329
+
330
+ ## Build & Bundle Requirements
331
+
332
+ The mobile admin bundle has its own Vite config: `vite.config.mobile-admin.ts`.
333
+
334
+ ### Build output
335
+
336
+ ```
337
+ dist/mobile-admin.umd.js
338
+ dist/mobile-admin.es.js
339
+ dist/mobile-admin.css (if needed)
340
+ ```
341
+
342
+ ### Example `vite.config.mobile-admin.ts`
343
+
344
+ ```typescript
345
+ import { defineConfig } from 'vite'
346
+ import react from '@vitejs/plugin-react'
347
+ import { resolve } from 'path'
348
+
349
+ const EXTERNALS: Record<string, string> = {
350
+ 'react': 'React',
351
+ 'react-dom': 'ReactDOM',
352
+ 'react/jsx-runtime': 'jsxRuntime',
353
+ '@proveanything/smartlinks': 'SL',
354
+ // Add all Radix, ReactQuery, ReactRouterDOM etc — same contract as containers
355
+ }
356
+
357
+ export default defineConfig({
358
+ plugins: [react()],
359
+ define: {
360
+ __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
361
+ __BUILD_DATE__: JSON.stringify(new Date().toISOString()),
362
+ },
363
+ build: {
364
+ lib: {
365
+ entry: resolve(__dirname, 'src/mobile-admin/index.ts'),
366
+ name: 'SmartLinksMobileAdmin',
367
+ formats: ['umd', 'es'],
368
+ fileName: (fmt) => `mobile-admin.${fmt === 'es' ? 'es' : 'umd'}.js`,
369
+ },
370
+ rollupOptions: {
371
+ external: Object.keys(EXTERNALS),
372
+ output: { globals: EXTERNALS },
373
+ },
374
+ },
375
+ })
376
+ ```
377
+
378
+ **Key points:**
379
+ - Externalise the same shared deps as `containers` (React, SL, Radix, ReactQuery, etc.) — the launcher provides them.
380
+ - Capacitor plugins (Tier 2) should be **bundled in**, not externalised — the browser/PWA fallback must work even when the native plugin is absent.
381
+ - Enable/disable the build with `VITE_ENABLE_MOBILE_ADMIN=true` in `.env`.
382
+
383
+ ### Build command
384
+
385
+ ```bash
386
+ # Full pipeline
387
+ vite build && \
388
+ vite build --config vite.config.widget.ts && \
389
+ vite build --config vite.config.container.ts && \
390
+ vite build --config vite.config.mobile-admin.ts
391
+
392
+ # Mobile admin only
393
+ vite build --config vite.config.mobile-admin.ts
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Example: Minimal Mobile Admin Container
399
+
400
+ ```typescript
401
+ // src/mobile-admin/WarehousePickContainer.tsx
402
+ import { useEffect, useState } from 'react'
403
+ import type { AdminMobileHostContext } from '@/lib/admin-mobile-host-context'
404
+
405
+ interface Props {
406
+ host: AdminMobileHostContext
407
+ }
408
+
409
+ export function WarehousePickContainer({ host }: Props) {
410
+ const [lastScan, setLastScan] = useState<string | null>(null)
411
+
412
+ // Auth guard — host may already enforce this, but be explicit
413
+ if (!host.user?.isAdmin) {
414
+ return <p>Admin access required.</p>
415
+ }
416
+
417
+ async function handleScan() {
418
+ try {
419
+ const code = await host.actions.requestQrScan()
420
+ setLastScan(code)
421
+ host.ui.haptic('success')
422
+ } catch (err) {
423
+ host.ui.toast({ title: 'Scan failed', variant: 'destructive' })
424
+ }
425
+ }
426
+
427
+ useEffect(() => {
428
+ host.ui.setHeaderTitle('Warehouse Pick')
429
+ return () => host.ui.setHeaderTitle(null)
430
+ }, [])
431
+
432
+ return (
433
+ <div style={{ padding: 16 }}>
434
+ {host.hardware.qr ? (
435
+ <button onClick={handleScan}>Scan Barcode</button>
436
+ ) : (
437
+ <p>Camera not available on this device</p>
438
+ )}
439
+ {lastScan && <p>Last scan: {lastScan}</p>}
440
+ </div>
441
+ )
442
+ }
443
+ ```
444
+
445
+ ```typescript
446
+ // src/mobile-admin/index.ts
447
+ export { WarehousePickContainer } from './WarehousePickContainer'
448
+
449
+ export const MOBILE_ADMIN_MANIFEST = {
450
+ name: 'WarehousePickContainer',
451
+ version: __APP_VERSION__,
452
+ buildDate: __BUILD_DATE__,
453
+ }
454
+ ```
455
+
456
+ ---
457
+
458
+ ## Best Practices
459
+
460
+ - **Always check `host.hardware.X` before calling `host.actions.X`** — never assume a capability is available.
461
+ - **Always wrap action calls in try/catch** — handle `HostPermissionDeniedError`, `HostTimeoutError`, and `HostCapabilityUnavailableError`.
462
+ - **Use `host.ui.toast`, `host.ui.haptic`, `host.ui.setHeaderTitle`** instead of mounting your own UI chrome — these integrate with the launcher's native UI.
463
+ - **Use `host.events.subscribe` for lifecycle events** — `'offline'`/`'online'`/`'pause'`/`'resume'` fire consistently on every host.
464
+ - **Never call `initializeApi`** — `host.SL` is already configured.
465
+ - **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
466
+ - **Declare `offline-queue` in capabilities** if your container queues writes — this signals the launcher to provision local storage support.
467
+ - **Use `host._version`** to feature-detect newer host context fields — breaking changes bump the major version and the launcher gates incompatible combinations.
@@ -23,16 +23,19 @@ Microapps are the **extensibility layer** of this ecosystem. Rather than buildin
23
23
 
24
24
  ### Deployment Modes
25
25
 
26
- Each microapp can be consumed in up to four ways:
26
+ Each microapp can be consumed in one or more of the following ways:
27
27
 
28
- | Mode | Embedding | Use Case |
29
- |------|-----------|---------|
30
- | **Iframe App** | Full React app in iframe | Complete experiences with routing, forms, complex UI |
31
- | **Widget** | React component in parent context | Lightweight previews, cards, summaries (~10 KB, loaded immediately) |
32
- | **Container** | Full app in parent React context | Complete experience without iframe (~150 KB+, lazy-loaded) |
33
- | **Executor** | JS library (no UI) | Programmatic config, SEO metadata, LLM content for AI/server |
28
+ | Mode | Description |
29
+ |------|-------------|
30
+ | **Container** | Full app in parent React tree. ~150 KB+ lazy-loaded. The primary consumer surface. |
31
+ | **Iframe App** | Full React app inside an iframe. Fallback when sandboxing is required; still the standard for setup admin screens. |
32
+ | **Widget** | Lightweight React component in parent tree. ~10 KB, loaded immediately alongside the page. |
33
+ | **Mobile Admin Container** | Separate bundle for in-the-field operator/admin workflows on mobile. Built independently; may use Capacitor, Preact, or any other runtime. |
34
+ | **Executor** | JS library, no UI. Programmatic config, SEO metadata, LLM content for AI/server. |
34
35
 
35
- Widgets and containers both run in the parent's React tree (not iframes). Executors have no UI — they expose functions that AI orchestrators and the server call directly.
36
+ Widgets and containers run in the parent's React tree (not iframes). Mobile Admin Containers are a separate bundle loaded by a dedicated mobile shell (Capacitor or PWA). Executors have no UI — they expose functions that AI orchestrators and the server call directly.
37
+
38
+ > **Admin vs consumer:** Admin experiences always ship as a **separate bundle** (`mobileAdmin` or iframe). The `containers` bundle is for consumer-facing surfaces only.
36
39
 
37
40
  ---
38
41
 
@@ -55,6 +58,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
55
58
  | **Internationalization** | `docs/i18n.md` | Adding multi-language support, translation patterns |
56
59
  | **Widgets** | `docs/widgets.md` | Building widgets, shared deps contract, settings schema |
57
60
  | **Containers** | `docs/containers.md` | Building full-app embeddable containers (lazy-loaded) |
61
+ | **Mobile Admin Container** | `docs/mobile-admin-container.md` | Building a separate Capacitor-aware mobile admin bundle for field operators |
58
62
  | **Executors** | `docs/executor.md` | Building executor bundles for SEO, LLM content, programmatic config |
59
63
  | **Deep Linking** | `docs/deep-link-discovery.md` | URL state management, navigable states, portal menus, AI nav |
60
64
  | **Interactions** | `docs/interactions.md` | Business events, outcomes, voting, competitions, and journey triggers |
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 1.10.2 | Generated: 2026-04-27T09:32:45.351Z
3
+ Version: 1.11.0 | Generated: 2026-04-30T11:35:44.821Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -91,6 +91,24 @@ The manifest is loaded automatically by the platform for every collection page.
91
91
  ]
92
92
  },
93
93
 
94
+ "mobileAdmin": {
95
+ "files": {
96
+ "js": {
97
+ "umd": "dist/mobile-admin.umd.js",
98
+ "esm": "dist/mobile-admin.es.js"
99
+ },
100
+ "css": null
101
+ },
102
+ "components": [
103
+ {
104
+ "name": "WarehousePickContainer",
105
+ "description": "In-field operator admin surface.",
106
+ "audience": "admin-mobile",
107
+ "capabilities": ["nfc", "qr", "offline-queue"]
108
+ }
109
+ ]
110
+ },
111
+
94
112
  "linkable": [
95
113
  { "title": "Home", "path": "/" },
96
114
  { "title": "Gallery", "path": "/gallery" },
@@ -188,7 +206,52 @@ This tells the platform and other apps that they can deep-link into a stored wid
188
206
  Same structure as `widgets` but declares the full-app container bundle. Lazy-loaded on demand.
189
207
 
190
208
  See the [Containers guide](containers.md) for details on the container component model.
209
+ **Component fields** (same as widgets, plus):
210
+
211
+ | Field | Type | Description |
212
+ |-------|------|--------------|
213
+ | `name` | string | Exported component name |
214
+ | `description` | string | Human-readable description |
215
+ | `props.required` / `props.optional` | string[] | Required and optional prop names |
216
+ | `audience` | `"public"` \| `"admin"` \| `"both"` | Who can use/see this component. Defaults to `"public"`. |
217
+ | `scope` | `"collection"` \| `"product"` | Data scope hint. `"product"` means the component always renders in the context of a specific product. |
218
+ | `settings` | object | JSON Schema describing configurable settings |
191
219
 
220
+ #### `mobileAdmin`
221
+
222
+ Declares a **separate** mobile admin bundle — a sibling of `containers` with its own build output. Use this when the mobile admin surface needs a different runtime, native-only dependencies (Capacitor), or independent versioning. Omit if your app has no mobile admin surface.
223
+
224
+ See [mobile-admin-container.md](mobile-admin-container.md) for the `AdminMobileHostContext` prop contract, the capability matrix, event stream, error types, and build setup.
225
+
226
+ ```json
227
+ "mobileAdmin": {
228
+ "files": {
229
+ "js": {
230
+ "umd": "dist/mobile-admin.umd.js",
231
+ "esm": "dist/mobile-admin.es.js"
232
+ },
233
+ "css": null
234
+ },
235
+ "components": [
236
+ {
237
+ "name": "WarehousePickContainer",
238
+ "description": "Pick orders by scanning NFC tags",
239
+ "audience": "admin-mobile",
240
+ "capabilities": ["nfc", "qr", "offline-queue"]
241
+ }
242
+ ]
243
+ }
244
+ ```
245
+
246
+ | Field | Description |
247
+ |-------|-------------|
248
+ | `files.js.umd` | UMD bundle path — used for dynamic `<script>` loading |
249
+ | `files.js.esm` | ESM bundle path (optional but recommended) |
250
+ | `files.css` | CSS bundle path — set to `null` if no styles |
251
+ | `components[].name` | Exported component name (must match the UMD bundle export) |
252
+ | `components[].description` | Shown in the mobile launcher's app picker |
253
+ | `components[].audience` | Must be `"admin-mobile"` |
254
+ | `components[].capabilities` | Hardware/feature capabilities this component needs or can use. See [capability list](mobile-admin-container.md#hardware-capabilities--the-capability-matrix). |
192
255
  #### `linkable`
193
256
 
194
257
  Static deep-linkable states built into the app — fixed routes that exist regardless of per-collection content. Declared once at build time.
@@ -446,3 +446,26 @@ src/containers/
446
446
  | Query cache conflicts | Sharing parent's QueryClient | Each container needs its own `QueryClient` instance |
447
447
  | `cva.cva` runtime error | Global set to lowercase `cva` | Use uppercase `CVA` for the global name |
448
448
  | Navigation does nothing | Using legacy string with `onNavigate` | Use structured `NavigationRequest` object instead |
449
+
450
+ ---
451
+
452
+ ## Multiple Consumer Components
453
+
454
+ A single `containers` bundle can export more than one component — useful when an app wants to offer different UX styles for different portal contexts (e.g. a portal card view vs a hub embed view). Each is a named export in the same bundle, declared as a separate entry in `components[]`:
455
+
456
+ ```json
457
+ "containers": {
458
+ "components": [
459
+ {
460
+ "name": "PortalContainer",
461
+ "description": "Full consumer experience for the SmartLinks portal"
462
+ },
463
+ {
464
+ "name": "HubContainer",
465
+ "description": "Compact consumer embed for hub-style layouts"
466
+ }
467
+ ]
468
+ }
469
+ ```
470
+
471
+ All components in `containers` are consumer-facing. **Admin and operator surfaces always ship as a separate bundle** (`mobileAdmin` or via iframe). See [mobile-admin-container.md](mobile-admin-container.md) for the separate-bundle approach.
@@ -0,0 +1,467 @@
1
+ # Mobile Admin Container SDK
2
+
3
+ > **Version:** 1.0 · **Platform:** SmartLinks R4 · **Last updated:** 2026-04-30
4
+
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
+
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.
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ 1. [When to Build a Mobile Admin Container](#when-to-build-a-mobile-admin-container)
14
+ 2. [Host Environment](#host-environment)
15
+ 3. [The `host` Prop](#the-host-prop)
16
+ 4. [Hardware Capabilities & the Capability Matrix](#hardware-capabilities--the-capability-matrix)
17
+ 5. [Capacitor Plugin Baseline](#capacitor-plugin-baseline)
18
+ 6. [Manifest Declaration](#manifest-declaration)
19
+ 7. [Event Stream](#event-stream)
20
+ 8. [Error Handling](#error-handling)
21
+ 9. [Lifecycle](#lifecycle)
22
+ 10. [Build & Bundle Requirements](#build--bundle-requirements)
23
+ 11. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
24
+ 12. [Best Practices](#best-practices)
25
+
26
+ ---
27
+
28
+ ## When to Build a Mobile Admin Container
29
+
30
+ Build a Mobile Admin Container when the surface is intended for **field operators or admins** rather than consumers — especially when:
31
+
32
+ - You need native hardware: NFC, RFID, camera/barcode, haptics, geolocation
33
+ - The surface must work offline or in flaky network conditions
34
+ - You want the bundle to version independently from the public consumer app
35
+
36
+ All admin-mobile containers ship in the `mobileAdmin` bundle and are never loaded by the public portal. The mobile launcher loads them via the `mobileAdmin` manifest key.
37
+
38
+ ---
39
+
40
+ ## Host Environment
41
+
42
+ The SmartLinks Mobile launcher runs on five host environments:
43
+
44
+ | Host ID | Description | Hardware bridge |
45
+ |---------|-------------|-----------------|
46
+ | `custom-android` | Original Kotlin WebView app — full RFID + NTAG-advanced | `window.SmartlinksScanner` |
47
+ | `capacitor-ios` | iOS app built via Capacitor | Capacitor plugins |
48
+ | `capacitor-android` | Generic Android Capacitor build (no RFID) | Capacitor plugins |
49
+ | `pwa` | Installed Progressive Web App | Web APIs only |
50
+ | `browser` | Regular browser tab | Web APIs only |
51
+
52
+ Your container **never** detects the host directly. It receives a `host` prop from the launcher and reads `host.hardware.*` capability flags. The launcher handles the rest.
53
+
54
+ ---
55
+
56
+ ## The `host` Prop
57
+
58
+ 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.
59
+
60
+ ```typescript
61
+ interface AdminMobileHostContext {
62
+ // Identity
63
+ collectionId: string
64
+ appId: string
65
+ user: { uid?: string; email?: string; displayName?: string; isAdmin: boolean } | null
66
+
67
+ // SDK — already initialised. Do NOT call initializeApi again.
68
+ SL: typeof import('@proveanything/smartlinks')
69
+
70
+ // Capabilities declared by this container in its manifest
71
+ capabilities: AdminMobileCapability[]
72
+
73
+ // Live capability flags for the current device
74
+ hardware: {
75
+ nfc: boolean
76
+ rfid: boolean
77
+ qr: boolean
78
+ camera: boolean
79
+ keyboard: boolean
80
+ }
81
+
82
+ // Unified hardware event stream
83
+ events: { subscribe: ScannerEventSubscriber }
84
+
85
+ // Promise-based hardware actions — reject with a structured error when unavailable
86
+ actions: {
87
+ requestQrScan: () => Promise<string>
88
+ requestNfcTap: (timeoutMs?: number) => Promise<{ uid: string; ndef?: string }>
89
+ requestCameraPhoto: () => Promise<Blob>
90
+ share: (payload: { title: string; url: string; text?: string }) => Promise<void>
91
+ clipboard: {
92
+ read: () => Promise<string>
93
+ write: (text: string) => Promise<void>
94
+ }
95
+ }
96
+
97
+ // Host-provided UI helpers
98
+ ui: {
99
+ toast: (opts: { title: string; description?: string; variant?: 'default' | 'destructive' }) => void
100
+ haptic: (style?: 'light' | 'success' | 'error') => void
101
+ setHeaderTitle: (title: string | null) => void
102
+ navigateBack: () => void
103
+ }
104
+
105
+ // Network & device info
106
+ network: { isOnline: () => boolean }
107
+ device: { info: () => Promise<{ model: string; platform: string }> }
108
+
109
+ // Host context version — for future feature-detection
110
+ _version: number
111
+ }
112
+ ```
113
+
114
+ ### Contract rules
115
+
116
+ 1. **Check `host.hardware.X` before calling `host.actions.X`.** Calls to unavailable capabilities reject with `HostCapabilityUnavailableError`.
117
+ 2. **Do not call `initializeApi`.** `host.SL` is already configured with the right `baseURL`, auth, and logger.
118
+ 3. **Do not access `window.SmartlinksScanner` or `window.Capacitor` directly.** The host wraps both; direct access produces containers that only work on one shell.
119
+ 4. **Externalise all shared deps** (React, ReactDOM, SL, Radix, etc.) — the launcher provides them as globals. See [Build & Bundle Requirements](#build--bundle-requirements).
120
+
121
+ ```typescript
122
+ // Example — always check hardware flags first
123
+ const { host } = props
124
+
125
+ if (host.hardware.nfc) {
126
+ host.ui.setHeaderTitle('Scan NFC tag')
127
+ const { uid } = await host.actions.requestNfcTap(5000)
128
+ // ...
129
+ }
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Hardware Capabilities & the Capability Matrix
135
+
136
+ Containers declare which capabilities they need (or can use) in the manifest `capabilities` array. The launcher uses this list to:
137
+
138
+ 1. **Filter availability** — hide containers whose required hardware is not on this device
139
+ 2. **Pre-warm** — start the right reader as soon as the container mounts
140
+ 3. **Permission prompts** — request only the permissions the container actually needs
141
+
142
+ | Capability | Description |
143
+ |------------|-------------|
144
+ | `"nfc"` | NFC tap — UID + NDEF read |
145
+ | `"nfc-advanced"` | NTAG 21x mirror config, NTAG 424 SUN, key rotation (`custom-android` only) |
146
+ | `"rfid"` | UHF RFID mass scan (`custom-android` only) |
147
+ | `"qr"` | QR / barcode scanning (camera-based) |
148
+ | `"camera"` | Photo capture, gallery picker |
149
+ | `"keyboard"` | Physical hardware trigger/action buttons |
150
+ | `"geolocation"` | GPS coordinates |
151
+ | `"offline-queue"` | Container needs local write queuing + sync |
152
+ | `"push"` | Remote push notifications (FCM/APNs) |
153
+
154
+ The full hardware capability matrix by host:
155
+
156
+ | Capability | custom-android | capacitor-ios | capacitor-android | pwa | browser |
157
+ |------------|:-:|:-:|:-:|:-:|:-:|
158
+ | NFC tap (UID + NDEF read) | ✅ | ✅ | ✅ | ⚠️ Chrome Android | ⚠️ |
159
+ | NFC NDEF write | ✅ | ❌ | ✅ | ⚠️ | ⚠️ |
160
+ | NFC advanced (NTAG 21x / 424) | ✅ | ❌ | ❌ | ❌ | ❌ |
161
+ | RFID mass scan | ✅ | ❌ | ❌ | ❌ | ❌ |
162
+ | QR / barcode scan | ✅ | ✅ | ✅ | ✅ | ✅ |
163
+ | Camera photo | ✅ | ✅ | ✅ | ✅ | ✅ |
164
+ | Hardware key events | ✅ | ❌ | ❌ | ❌ | ❌ |
165
+ | Haptics | ✅ | ✅ | ✅ | ⚠️ visual only | ⚠️ |
166
+
167
+ > Containers that declare `"rfid"` or `"nfc-advanced"` are only offered on `custom-android`. All others are assumed to run on all five hosts — design for the lowest common denominator and use `host.hardware.*` flags to unlock richer UI on capable devices.
168
+
169
+ ---
170
+
171
+ ## Capacitor Plugin Baseline
172
+
173
+ The custom Kotlin shell and both Capacitor shells ship the same baseline plugin set. Container authors can rely on all Tier 1 plugins on every native host.
174
+
175
+ ### Tier 1 — always available on every native host
176
+
177
+ | Plugin | Surfaced via |
178
+ |--------|-------------|
179
+ | `@capacitor/haptics` | `host.ui.haptic()` |
180
+ | `@capacitor/network` | `host.network.isOnline()` |
181
+ | `@capacitor/device` | `host.device.info()` |
182
+ | `@capacitor/share` | `host.actions.share()` |
183
+ | `@capacitor/clipboard` | `host.actions.clipboard.*` |
184
+ | `@capacitor/preferences` | `host.storage.*` *(planned)* |
185
+ | `@capacitor/app` | host-managed (back button, deep links) |
186
+ | `@capacitor/status-bar` | host-managed |
187
+ | `@capacitor/keyboard` | host-managed |
188
+ | `@capacitor/toast` | wired into `host.ui.toast()` |
189
+
190
+ ### Tier 2 — capability-gated (declare in manifest)
191
+
192
+ | Plugin | Capability flag |
193
+ |--------|----------------|
194
+ | `@capacitor/camera` | `"camera"` |
195
+ | `@capacitor-mlkit/barcode-scanning` | `"qr"` |
196
+ | `@capgo/capacitor-nfc` | `"nfc"` |
197
+ | `@capacitor/geolocation` | `"geolocation"` |
198
+ | `@capacitor/push-notifications` | `"push"` |
199
+ | `@capacitor/filesystem` | *(declare `"offline-queue"` if needed)* |
200
+
201
+ ### Tier 3 — custom-android only (not Capacitor)
202
+
203
+ Proprietary hardware SDKs (UHF RFID, NTAG 21x/424 advanced programming, USB uFR reader feedback). These are wrapped behind `host.actions.*` — never access `window.SmartlinksScanner` directly.
204
+
205
+ ---
206
+
207
+ ## Manifest Declaration
208
+
209
+ Declare the bundle under the top-level `mobileAdmin` key in `app.manifest.json`. This is separate from `containers` — the mobile launcher reads `mobileAdmin`; the public portal never loads it.
210
+
211
+ ```json
212
+ {
213
+ "meta": { "appId": "my-app", "name": "My App", "version": "1.0.0" },
214
+
215
+ "containers": {
216
+ "files": { "js": { "umd": "dist/containers.umd.js", "esm": "dist/containers.es.js" }, "css": "dist/containers.css" },
217
+ "components": [
218
+ { "name": "PublicContainer", "description": "Default consumer experience" }
219
+ ]
220
+ },
221
+
222
+ "mobileAdmin": {
223
+ "files": {
224
+ "js": {
225
+ "umd": "dist/mobile-admin.umd.js",
226
+ "esm": "dist/mobile-admin.es.js"
227
+ },
228
+ "css": null
229
+ },
230
+ "components": [
231
+ {
232
+ "name": "WarehousePickContainer",
233
+ "description": "Pick orders by scanning NFC tags",
234
+ "audience": "admin-mobile",
235
+ "capabilities": ["nfc", "qr", "offline-queue"]
236
+ }
237
+ ]
238
+ }
239
+ }
240
+ ```
241
+
242
+ A `mobileAdmin` bundle can export multiple components targeting different operator workflows — each with its own `capabilities` declaration.
243
+
244
+ **Component fields:**
245
+
246
+ | Field | Type | Description |
247
+ |-------|------|-------------|
248
+ | `name` | string | Exported component name (must match the UMD bundle export) |
249
+ | `description` | string | Shown in the mobile launcher's app picker |
250
+ | `audience` | `"admin-mobile"` | Required — tells the launcher this is an operator surface |
251
+ | `capabilities` | string[] | Capabilities this component needs or can use. See table above. |
252
+
253
+ ---
254
+
255
+ ## Event Stream
256
+
257
+ 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.
258
+
259
+ ```typescript
260
+ useEffect(() => {
261
+ const unsubscribe = host.events.subscribe((event) => {
262
+ switch (event.type) {
263
+ case 'nfc-tap': handleNfc(event.uid, event.ndef); break
264
+ case 'rfid-burst': handleEpcs(event.epcs); break
265
+ case 'qr-scan': handleQr(event.code); break
266
+ case 'key-press': if (event.keyCode === 293) startScan(); break
267
+ case 'lifecycle': handleLifecycle(event.phase); break
268
+ }
269
+ })
270
+ return unsubscribe
271
+ }, [host.events])
272
+ ```
273
+
274
+ 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.
275
+
276
+ ---
277
+
278
+ ## Error Handling
279
+
280
+ Action methods reject with structured errors — wrap every `host.actions.*` call even on hosts where the capability "should" be available (the user can deny permission at runtime):
281
+
282
+ ```typescript
283
+ class HostCapabilityUnavailableError extends Error {
284
+ capability: 'nfc' | 'rfid' | 'qr' | 'camera'
285
+ host: string // host ID
286
+ }
287
+
288
+ class HostPermissionDeniedError extends Error {
289
+ capability: 'nfc' | 'rfid' | 'qr' | 'camera'
290
+ }
291
+
292
+ class HostTimeoutError extends Error {
293
+ capability: 'nfc' | 'qr'
294
+ timeoutMs: number
295
+ }
296
+ ```
297
+
298
+ ```typescript
299
+ try {
300
+ const code = await host.actions.requestQrScan()
301
+ host.ui.haptic('success')
302
+ } catch (err) {
303
+ if (err instanceof HostPermissionDeniedError) {
304
+ host.ui.toast({ title: 'Camera access required', variant: 'destructive' })
305
+ } else if (err instanceof HostTimeoutError) {
306
+ host.ui.toast({ title: 'No QR code detected' })
307
+ } else {
308
+ throw err
309
+ }
310
+ }
311
+ ```
312
+
313
+ ---
314
+
315
+ ## Lifecycle
316
+
317
+ | Event | When | What to do |
318
+ |-------|------|------------|
319
+ | mount | User opens your container | Subscribe to events, start readers |
320
+ | unmount | User backs out / app backgrounded > 30s | Unsubscribe; persist in-flight state |
321
+ | `lifecycle: 'offline'` | Network lost | Switch to offline-queue mode |
322
+ | `lifecycle: 'online'` | Network restored | Flush queued writes |
323
+ | `lifecycle: 'pause'` | App backgrounded | Pause readers to save battery |
324
+ | `lifecycle: 'resume'` | App foregrounded | Resubscribe; refresh stale data |
325
+
326
+ Use `host.events.subscribe` for all lifecycle events — it fires identically on every host.
327
+
328
+ ---
329
+
330
+ ## Build & Bundle Requirements
331
+
332
+ The mobile admin bundle has its own Vite config: `vite.config.mobile-admin.ts`.
333
+
334
+ ### Build output
335
+
336
+ ```
337
+ dist/mobile-admin.umd.js
338
+ dist/mobile-admin.es.js
339
+ dist/mobile-admin.css (if needed)
340
+ ```
341
+
342
+ ### Example `vite.config.mobile-admin.ts`
343
+
344
+ ```typescript
345
+ import { defineConfig } from 'vite'
346
+ import react from '@vitejs/plugin-react'
347
+ import { resolve } from 'path'
348
+
349
+ const EXTERNALS: Record<string, string> = {
350
+ 'react': 'React',
351
+ 'react-dom': 'ReactDOM',
352
+ 'react/jsx-runtime': 'jsxRuntime',
353
+ '@proveanything/smartlinks': 'SL',
354
+ // Add all Radix, ReactQuery, ReactRouterDOM etc — same contract as containers
355
+ }
356
+
357
+ export default defineConfig({
358
+ plugins: [react()],
359
+ define: {
360
+ __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
361
+ __BUILD_DATE__: JSON.stringify(new Date().toISOString()),
362
+ },
363
+ build: {
364
+ lib: {
365
+ entry: resolve(__dirname, 'src/mobile-admin/index.ts'),
366
+ name: 'SmartLinksMobileAdmin',
367
+ formats: ['umd', 'es'],
368
+ fileName: (fmt) => `mobile-admin.${fmt === 'es' ? 'es' : 'umd'}.js`,
369
+ },
370
+ rollupOptions: {
371
+ external: Object.keys(EXTERNALS),
372
+ output: { globals: EXTERNALS },
373
+ },
374
+ },
375
+ })
376
+ ```
377
+
378
+ **Key points:**
379
+ - Externalise the same shared deps as `containers` (React, SL, Radix, ReactQuery, etc.) — the launcher provides them.
380
+ - Capacitor plugins (Tier 2) should be **bundled in**, not externalised — the browser/PWA fallback must work even when the native plugin is absent.
381
+ - Enable/disable the build with `VITE_ENABLE_MOBILE_ADMIN=true` in `.env`.
382
+
383
+ ### Build command
384
+
385
+ ```bash
386
+ # Full pipeline
387
+ vite build && \
388
+ vite build --config vite.config.widget.ts && \
389
+ vite build --config vite.config.container.ts && \
390
+ vite build --config vite.config.mobile-admin.ts
391
+
392
+ # Mobile admin only
393
+ vite build --config vite.config.mobile-admin.ts
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Example: Minimal Mobile Admin Container
399
+
400
+ ```typescript
401
+ // src/mobile-admin/WarehousePickContainer.tsx
402
+ import { useEffect, useState } from 'react'
403
+ import type { AdminMobileHostContext } from '@/lib/admin-mobile-host-context'
404
+
405
+ interface Props {
406
+ host: AdminMobileHostContext
407
+ }
408
+
409
+ export function WarehousePickContainer({ host }: Props) {
410
+ const [lastScan, setLastScan] = useState<string | null>(null)
411
+
412
+ // Auth guard — host may already enforce this, but be explicit
413
+ if (!host.user?.isAdmin) {
414
+ return <p>Admin access required.</p>
415
+ }
416
+
417
+ async function handleScan() {
418
+ try {
419
+ const code = await host.actions.requestQrScan()
420
+ setLastScan(code)
421
+ host.ui.haptic('success')
422
+ } catch (err) {
423
+ host.ui.toast({ title: 'Scan failed', variant: 'destructive' })
424
+ }
425
+ }
426
+
427
+ useEffect(() => {
428
+ host.ui.setHeaderTitle('Warehouse Pick')
429
+ return () => host.ui.setHeaderTitle(null)
430
+ }, [])
431
+
432
+ return (
433
+ <div style={{ padding: 16 }}>
434
+ {host.hardware.qr ? (
435
+ <button onClick={handleScan}>Scan Barcode</button>
436
+ ) : (
437
+ <p>Camera not available on this device</p>
438
+ )}
439
+ {lastScan && <p>Last scan: {lastScan}</p>}
440
+ </div>
441
+ )
442
+ }
443
+ ```
444
+
445
+ ```typescript
446
+ // src/mobile-admin/index.ts
447
+ export { WarehousePickContainer } from './WarehousePickContainer'
448
+
449
+ export const MOBILE_ADMIN_MANIFEST = {
450
+ name: 'WarehousePickContainer',
451
+ version: __APP_VERSION__,
452
+ buildDate: __BUILD_DATE__,
453
+ }
454
+ ```
455
+
456
+ ---
457
+
458
+ ## Best Practices
459
+
460
+ - **Always check `host.hardware.X` before calling `host.actions.X`** — never assume a capability is available.
461
+ - **Always wrap action calls in try/catch** — handle `HostPermissionDeniedError`, `HostTimeoutError`, and `HostCapabilityUnavailableError`.
462
+ - **Use `host.ui.toast`, `host.ui.haptic`, `host.ui.setHeaderTitle`** instead of mounting your own UI chrome — these integrate with the launcher's native UI.
463
+ - **Use `host.events.subscribe` for lifecycle events** — `'offline'`/`'online'`/`'pause'`/`'resume'` fire consistently on every host.
464
+ - **Never call `initializeApi`** — `host.SL` is already configured.
465
+ - **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
466
+ - **Declare `offline-queue` in capabilities** if your container queues writes — this signals the launcher to provision local storage support.
467
+ - **Use `host._version`** to feature-detect newer host context fields — breaking changes bump the major version and the launcher gates incompatible combinations.
package/docs/overview.md CHANGED
@@ -23,16 +23,19 @@ Microapps are the **extensibility layer** of this ecosystem. Rather than buildin
23
23
 
24
24
  ### Deployment Modes
25
25
 
26
- Each microapp can be consumed in up to four ways:
26
+ Each microapp can be consumed in one or more of the following ways:
27
27
 
28
- | Mode | Embedding | Use Case |
29
- |------|-----------|---------|
30
- | **Iframe App** | Full React app in iframe | Complete experiences with routing, forms, complex UI |
31
- | **Widget** | React component in parent context | Lightweight previews, cards, summaries (~10 KB, loaded immediately) |
32
- | **Container** | Full app in parent React context | Complete experience without iframe (~150 KB+, lazy-loaded) |
33
- | **Executor** | JS library (no UI) | Programmatic config, SEO metadata, LLM content for AI/server |
28
+ | Mode | Description |
29
+ |------|-------------|
30
+ | **Container** | Full app in parent React tree. ~150 KB+ lazy-loaded. The primary consumer surface. |
31
+ | **Iframe App** | Full React app inside an iframe. Fallback when sandboxing is required; still the standard for setup admin screens. |
32
+ | **Widget** | Lightweight React component in parent tree. ~10 KB, loaded immediately alongside the page. |
33
+ | **Mobile Admin Container** | Separate bundle for in-the-field operator/admin workflows on mobile. Built independently; may use Capacitor, Preact, or any other runtime. |
34
+ | **Executor** | JS library, no UI. Programmatic config, SEO metadata, LLM content for AI/server. |
34
35
 
35
- Widgets and containers both run in the parent's React tree (not iframes). Executors have no UI — they expose functions that AI orchestrators and the server call directly.
36
+ Widgets and containers run in the parent's React tree (not iframes). Mobile Admin Containers are a separate bundle loaded by a dedicated mobile shell (Capacitor or PWA). Executors have no UI — they expose functions that AI orchestrators and the server call directly.
37
+
38
+ > **Admin vs consumer:** Admin experiences always ship as a **separate bundle** (`mobileAdmin` or iframe). The `containers` bundle is for consumer-facing surfaces only.
36
39
 
37
40
  ---
38
41
 
@@ -55,6 +58,7 @@ The SmartLinks SDK (`@proveanything/smartlinks`) includes comprehensive document
55
58
  | **Internationalization** | `docs/i18n.md` | Adding multi-language support, translation patterns |
56
59
  | **Widgets** | `docs/widgets.md` | Building widgets, shared deps contract, settings schema |
57
60
  | **Containers** | `docs/containers.md` | Building full-app embeddable containers (lazy-loaded) |
61
+ | **Mobile Admin Container** | `docs/mobile-admin-container.md` | Building a separate Capacitor-aware mobile admin bundle for field operators |
58
62
  | **Executors** | `docs/executor.md` | Building executor bundles for SEO, LLM content, programmatic config |
59
63
  | **Deep Linking** | `docs/deep-link-discovery.md` | URL state management, navigable states, portal menus, AI nav |
60
64
  | **Interactions** | `docs/interactions.md` | Business events, outcomes, voting, competitions, and journey triggers |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "1.10.2",
3
+ "version": "1.11.0",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",