@proveanything/smartlinks 1.10.3 → 1.11.1

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.3 | Generated: 2026-04-27T10:04:40.477Z
3
+ Version: 1.11.1 | Generated: 2026-04-30T12:17:30.051Z
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
+ "capabilities": ["nfc", "qr"],
107
+ "offline": true
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
+ "capabilities": ["nfc", "qr"],
240
+ "offline": true
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[].capabilities` | Hardware capabilities this component needs or can use. See [capability list](mobile-admin-container.md#hardware-capabilities--the-capability-matrix). |
254
+ | `components[].offline` | Set to `true` if this component queues writes locally and needs offline sync support. |
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,528 @@
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. [Launcher Discovery](#launcher-discovery)
20
+ 8. [Event Stream](#event-stream)
21
+ 9. [Error Handling](#error-handling)
22
+ 10. [Lifecycle](#lifecycle)
23
+ 11. [Build & Bundle Requirements](#build--bundle-requirements)
24
+ 12. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
25
+ 13. [Best Practices](#best-practices)
26
+
27
+ ---
28
+
29
+ ## When to Build a Mobile Admin Container
30
+
31
+ Build a Mobile Admin Container when the surface is intended for **field operators or admins** rather than consumers — especially when:
32
+
33
+ - You need native hardware: NFC, RFID, camera/barcode, haptics, geolocation
34
+ - The surface must work offline or in flaky network conditions
35
+ - You want the bundle to version independently from the public consumer app
36
+
37
+ 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.
38
+
39
+ ---
40
+
41
+ ## Host Environment
42
+
43
+ The SmartLinks Mobile launcher runs on five host environments:
44
+
45
+ | Host ID | Description | Hardware bridge |
46
+ |---------|-------------|-----------------|
47
+ | `custom-android` | Original Kotlin WebView app — full RFID + NTAG-advanced | `window.SmartlinksScanner` |
48
+ | `capacitor-ios` | iOS app built via Capacitor | Capacitor plugins |
49
+ | `capacitor-android` | Generic Android Capacitor build (no RFID) | Capacitor plugins |
50
+ | `pwa` | Installed Progressive Web App | Web APIs only |
51
+ | `browser` | Regular browser tab | Web APIs only |
52
+
53
+ 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.
54
+
55
+ ---
56
+
57
+ ## The `host` Prop
58
+
59
+ 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
+ ```typescript
62
+ interface AdminMobileHostContext {
63
+ // Identity
64
+ collectionId: string
65
+ appId: string
66
+ user: { uid?: string; email?: string; displayName?: string; isAdmin: boolean } | null
67
+
68
+ // SDK — already initialised. Do NOT call initializeApi again.
69
+ SL: typeof import('@proveanything/smartlinks')
70
+
71
+ // Capabilities declared by this container in its manifest
72
+ capabilities: AdminMobileCapability[]
73
+
74
+ // Live capability flags for the current device
75
+ hardware: {
76
+ nfc: boolean
77
+ rfid: boolean
78
+ qr: boolean
79
+ camera: boolean
80
+ keyboard: boolean
81
+ }
82
+
83
+ // Unified hardware event stream
84
+ events: { subscribe: ScannerEventSubscriber }
85
+
86
+ // Promise-based hardware actions — reject with a structured error when unavailable
87
+ actions: {
88
+ requestQrScan: () => Promise<string>
89
+ requestNfcTap: (timeoutMs?: number) => Promise<{ uid: string; ndef?: string }>
90
+ requestCameraPhoto: () => Promise<Blob>
91
+ share: (payload: { title: string; url: string; text?: string }) => Promise<void>
92
+ clipboard: {
93
+ read: () => Promise<string>
94
+ write: (text: string) => Promise<void>
95
+ }
96
+ }
97
+
98
+ // Host-provided UI helpers
99
+ ui: {
100
+ toast: (opts: { title: string; description?: string; variant?: 'default' | 'destructive' }) => void
101
+ haptic: (style?: 'light' | 'success' | 'error') => void
102
+ setHeaderTitle: (title: string | null) => void
103
+ navigateBack: () => void
104
+ }
105
+
106
+ // Network & device info
107
+ network: { isOnline: () => boolean }
108
+ device: { info: () => Promise<{ model: string; platform: string }> }
109
+
110
+ // Informational host version — use for logging/diagnostics, not feature detection.
111
+ // For feature detection prefer existence checks: 'requestNfcTap' in host.actions
112
+ _version: number
113
+ }
114
+ ```
115
+
116
+ ### Contract rules
117
+
118
+ 1. **Check `host.hardware.X` before calling `host.actions.X`.** Calls to unavailable capabilities reject with `HostCapabilityUnavailableError`.
119
+ 2. **Do not call `initializeApi`, and do not use top-level SDK imports for API calls.** `host.SL` is already configured with the right `baseURL`, auth, and logger. Code that does `import * as SL from "@proveanything/smartlinks"` and calls `SL.attestation.create(...)` will silently hit the wrong base URL — the top-level import is uninitialised in the launcher's runtime and produces no loud error. Always go through `host.SL`:
120
+ ```typescript
121
+ // ❌ Silent footgun — uninitialised SDK, wrong baseURL
122
+ import * as SL from '@proveanything/smartlinks'
123
+ await SL.attestation.create(...)
124
+
125
+ // ✅ Correct
126
+ await host.SL.attestation.create(...)
127
+ ```
128
+ 3. **Do not access `window.SmartlinksScanner` or `window.Capacitor` directly.** The host wraps both; direct access produces containers that only work on one shell.
129
+ 4. **Externalise all shared deps** (React, ReactDOM, SL, Radix, etc.) — the launcher provides them as globals. See [Build & Bundle Requirements](#build--bundle-requirements).
130
+
131
+ ```typescript
132
+ // Example — always check hardware flags first
133
+ const { host } = props
134
+
135
+ if (host.hardware.nfc) {
136
+ host.ui.setHeaderTitle('Scan NFC tag')
137
+ const { uid } = await host.actions.requestNfcTap(5000)
138
+ // ...
139
+ }
140
+ ```
141
+
142
+ ### `host.ui` — native helpers vs. your own components
143
+
144
+ `host.ui.setHeaderTitle()` and `host.ui.navigateBack()` are **host-only** — there is no in-container equivalent. Call them via `host.ui` or omit them.
145
+
146
+ `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.
147
+
148
+ **Stub pattern for testing and Storybook:**
149
+
150
+ ```typescript
151
+ const stubHost: Partial<AdminMobileHostContext> = {
152
+ hardware: { nfc: false, rfid: false, qr: true, camera: true, keyboard: false },
153
+ actions: {
154
+ requestQrScan: async () => 'STUB_QR_CODE',
155
+ requestNfcTap: async () => ({ uid: 'STUB_UID' }),
156
+ requestCameraPhoto: async () => new Blob(),
157
+ share: async () => {},
158
+ clipboard: { read: async () => '', write: async () => {} },
159
+ },
160
+ ui: {
161
+ toast: (opts) => console.log('[stub toast]', opts.title),
162
+ haptic: () => {},
163
+ setHeaderTitle: (t) => { if (t) document.title = t },
164
+ navigateBack: () => history.back(),
165
+ },
166
+ network: { isOnline: () => true },
167
+ user: { isAdmin: true, displayName: 'Test User', email: 'test@example.com' },
168
+ SL: undefined as any, // replace with your test SL instance
169
+ }
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Hardware Capabilities & the Capability Matrix
175
+
176
+ Containers declare which capabilities they need (or can use) in the manifest `capabilities` array. The launcher uses this list to:
177
+
178
+ 1. **Filter availability** — hide containers whose required hardware is not on this device
179
+ 2. **Pre-warm** — start the right reader as soon as the container mounts
180
+ 3. **Permission prompts** — request only the permissions the container actually needs
181
+
182
+ | Capability | Description |
183
+ |------------|-------------|
184
+ | `"nfc"` | NFC tap — UID + NDEF read |
185
+ | `"nfc-advanced"` | NTAG 21x mirror config, NTAG 424 SUN, key rotation (`custom-android` only) |
186
+ | `"rfid"` | UHF RFID mass scan (`custom-android` only) |
187
+ | `"qr"` | QR / barcode scanning (camera-based) |
188
+ | `"camera"` | Photo capture, gallery picker |
189
+ | `"keyboard"` | Physical hardware trigger/action buttons |
190
+ | `"geolocation"` | GPS coordinates |
191
+ | `"push"` | Remote push notifications (FCM/APNs) |
192
+
193
+ The full hardware capability matrix by host:
194
+
195
+ | Capability | custom-android | capacitor-ios | capacitor-android | pwa | browser |
196
+ |------------|:-:|:-:|:-:|:-:|:-:|
197
+ | NFC tap (UID + NDEF read) | ✅ | ✅ | ✅ | ⚠️ Chrome Android | ⚠️ |
198
+ | NFC NDEF write | ✅ | ❌ | ✅ | ⚠️ | ⚠️ |
199
+ | NFC advanced (NTAG 21x / 424) | ✅ | ❌ | ❌ | ❌ | ❌ |
200
+ | RFID mass scan | ✅ | ❌ | ❌ | ❌ | ❌ |
201
+ | QR / barcode scan | ✅ | ✅ | ✅ | ✅ | ✅ |
202
+ | Camera photo | ✅ | ✅ | ✅ | ✅ | ✅ |
203
+ | Hardware key events | ✅ | ❌ | ❌ | ❌ | ❌ |
204
+ | Haptics | ✅ | ✅ | ✅ | ⚠️ visual only | ⚠️ |
205
+
206
+ > 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.
207
+
208
+ ---
209
+
210
+ ## Capacitor Plugin Baseline
211
+
212
+ 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.
213
+
214
+ ### Tier 1 — always available on every native host
215
+
216
+ | Plugin | Surfaced via |
217
+ |--------|-------------|
218
+ | `@capacitor/haptics` | `host.ui.haptic()` |
219
+ | `@capacitor/network` | `host.network.isOnline()` |
220
+ | `@capacitor/device` | `host.device.info()` |
221
+ | `@capacitor/share` | `host.actions.share()` |
222
+ | `@capacitor/clipboard` | `host.actions.clipboard.*` |
223
+ | `@capacitor/preferences` | `host.storage.*` *(planned)* |
224
+ | `@capacitor/app` | host-managed (back button, deep links) |
225
+ | `@capacitor/status-bar` | host-managed |
226
+ | `@capacitor/keyboard` | host-managed |
227
+ | `@capacitor/toast` | wired into `host.ui.toast()` |
228
+
229
+ ### Tier 2 — capability-gated (declare in manifest)
230
+
231
+ | Plugin | Capability flag |
232
+ |--------|----------------|
233
+ | `@capacitor/camera` | `"camera"` |
234
+ | `@capacitor-mlkit/barcode-scanning` | `"qr"` |
235
+ | `@capgo/capacitor-nfc` | `"nfc"` |
236
+ | `@capacitor/geolocation` | `"geolocation"` |
237
+ | `@capacitor/push-notifications` | `"push"` |
238
+ | `@capacitor/filesystem` | *(used when `offline: true`; bundle it in)* |
239
+
240
+ ### Tier 3 — custom-android only (not Capacitor)
241
+
242
+ 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.
243
+
244
+ ---
245
+
246
+ ## Manifest Declaration
247
+
248
+ 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.
249
+
250
+ ```json
251
+ {
252
+ "meta": { "appId": "my-app", "name": "My App", "version": "1.0.0" },
253
+
254
+ "containers": {
255
+ "files": { "js": { "umd": "dist/containers.umd.js", "esm": "dist/containers.es.js" }, "css": "dist/containers.css" },
256
+ "components": [
257
+ { "name": "PublicContainer", "description": "Default consumer experience" }
258
+ ]
259
+ },
260
+
261
+ "mobileAdmin": {
262
+ "files": {
263
+ "js": {
264
+ "umd": "dist/mobile-admin.umd.js",
265
+ "esm": "dist/mobile-admin.es.js"
266
+ },
267
+ "css": null
268
+ },
269
+ "components": [
270
+ {
271
+ "name": "WarehousePickContainer",
272
+ "description": "Pick orders by scanning NFC tags",
273
+ "capabilities": ["nfc", "qr"],
274
+ "offline": true
275
+ }
276
+ ]
277
+ }
278
+ }
279
+ ```
280
+
281
+ A `mobileAdmin` bundle can export multiple components targeting different operator workflows — each with its own `capabilities` declaration.
282
+
283
+ **Component fields:**
284
+
285
+ | Field | Type | Description |
286
+ |-------|------|-------------|
287
+ | `name` | string | Exported component name (must match the UMD bundle export) |
288
+ | `description` | string | Shown in the mobile launcher's app picker |
289
+ | `capabilities` | string[] | Hardware capabilities this component needs or can use. See table above. |
290
+ | `offline` | boolean | Set to `true` if this component queues writes locally and needs offline sync support. |
291
+
292
+ ---
293
+
294
+ ## Launcher Discovery
295
+
296
+ The SmartLinks Mobile launcher loads your `mobileAdmin` bundle through the platform's app registry — you do not reference the manifest URL directly.
297
+
298
+ **How it works:**
299
+
300
+ 1. A collection admin enables the app for a collection in the SmartLinks admin console.
301
+ 2. On startup (and periodically), the launcher fetches `app.manifest.json` for every enabled app in that collection via the SmartLinks platform API. These requests are authenticated with the launcher's operator session.
302
+ 3. The launcher reads `mobileAdmin.components[]`, evaluates each component's `capabilities` against the current device's hardware flags, and shows matching components in the app picker.
303
+ 4. When the operator opens a component, the launcher resolves `mobileAdmin.files.js.umd` against the app's CDN base, fetches the bundle, and mounts the component with its `host` prop.
304
+
305
+ **File URL resolution:** `files.js.umd` / `files.js.esm` are paths relative to your app's CDN root (set when the app version is published). Provide relative paths — the launcher resolves them; do not hardcode absolute URLs.
306
+
307
+ **Auth:** Manifest and bundle fetches use the launcher's session token. Your container is already authenticated via `host.user` and `host.SL` — do not add additional auth headers.
308
+
309
+ **On load failure:** If the bundle returns a non-2xx response, the launcher shows a user-visible error and logs it — it does not crash the full launcher. If `app.manifest.json` itself cannot be fetched, the app is silently excluded from the picker for that session.
310
+
311
+ > To debug a missing or stale manifest, confirm in the SmartLinks admin console that the app version is published and that the `mobileAdmin` key is present in the uploaded manifest.
312
+
313
+ ---
314
+
315
+ ## Event Stream
316
+
317
+ 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.
318
+
319
+ ```typescript
320
+ useEffect(() => {
321
+ const unsubscribe = host.events.subscribe((event) => {
322
+ switch (event.type) {
323
+ case 'nfc-tap': handleNfc(event.uid, event.ndef); break
324
+ case 'rfid-burst': handleEpcs(event.epcs); break
325
+ case 'qr-scan': handleQr(event.code); break
326
+ case 'key-press': if (event.keyCode === 293) startScan(); break
327
+ case 'lifecycle': handleLifecycle(event.phase); break
328
+ }
329
+ })
330
+ return unsubscribe
331
+ }, [host.events])
332
+ ```
333
+
334
+ 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.
335
+
336
+ ---
337
+
338
+ ## Error Handling
339
+
340
+ 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):
341
+
342
+ ```typescript
343
+ class HostCapabilityUnavailableError extends Error {
344
+ capability: 'nfc' | 'rfid' | 'qr' | 'camera'
345
+ host: string // host ID
346
+ }
347
+
348
+ class HostPermissionDeniedError extends Error {
349
+ capability: 'nfc' | 'rfid' | 'qr' | 'camera'
350
+ }
351
+
352
+ class HostTimeoutError extends Error {
353
+ capability: 'nfc' | 'qr'
354
+ timeoutMs: number
355
+ }
356
+ ```
357
+
358
+ ```typescript
359
+ try {
360
+ const code = await host.actions.requestQrScan()
361
+ host.ui.haptic('success')
362
+ } catch (err) {
363
+ if (err instanceof HostPermissionDeniedError) {
364
+ host.ui.toast({ title: 'Camera access required', variant: 'destructive' })
365
+ } else if (err instanceof HostTimeoutError) {
366
+ host.ui.toast({ title: 'No QR code detected' })
367
+ } else {
368
+ throw err
369
+ }
370
+ }
371
+ ```
372
+
373
+ ---
374
+
375
+ ## Lifecycle
376
+
377
+ | Event | When | What to do |
378
+ |-------|------|------------|
379
+ | mount | User opens your container | Subscribe to events, start readers |
380
+ | unmount | User backs out / app backgrounded > 30s | Unsubscribe; persist in-flight state |
381
+ | `lifecycle: 'offline'` | Network lost | Switch to offline mode; queue writes locally |
382
+ | `lifecycle: 'online'` | Network restored | Flush queued writes |
383
+ | `lifecycle: 'pause'` | App backgrounded | Pause readers to save battery |
384
+ | `lifecycle: 'resume'` | App foregrounded | Resubscribe; refresh stale data |
385
+
386
+ Use `host.events.subscribe` for all lifecycle events — it fires identically on every host.
387
+
388
+ ---
389
+
390
+ ## Build & Bundle Requirements
391
+
392
+ The mobile admin bundle has its own Vite config: `vite.config.mobile-admin.ts`.
393
+
394
+ ### Build output
395
+
396
+ ```
397
+ dist/mobile-admin.umd.js
398
+ dist/mobile-admin.es.js
399
+ dist/mobile-admin.css (if needed)
400
+ ```
401
+
402
+ ### Example `vite.config.mobile-admin.ts`
403
+
404
+ ```typescript
405
+ import { defineConfig } from 'vite'
406
+ import react from '@vitejs/plugin-react'
407
+ import { resolve } from 'path'
408
+
409
+ const EXTERNALS: Record<string, string> = {
410
+ 'react': 'React',
411
+ 'react-dom': 'ReactDOM',
412
+ 'react/jsx-runtime': 'jsxRuntime',
413
+ '@proveanything/smartlinks': 'SL',
414
+ // Add all Radix, ReactQuery, ReactRouterDOM etc — same contract as containers
415
+ }
416
+
417
+ export default defineConfig({
418
+ plugins: [react()],
419
+ define: {
420
+ __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
421
+ __BUILD_DATE__: JSON.stringify(new Date().toISOString()),
422
+ },
423
+ build: {
424
+ lib: {
425
+ entry: resolve(__dirname, 'src/mobile-admin/index.ts'),
426
+ name: 'SmartLinksMobileAdmin',
427
+ formats: ['umd', 'es'],
428
+ fileName: (fmt) => `mobile-admin.${fmt === 'es' ? 'es' : 'umd'}.js`,
429
+ },
430
+ rollupOptions: {
431
+ external: Object.keys(EXTERNALS),
432
+ output: { globals: EXTERNALS },
433
+ },
434
+ },
435
+ })
436
+ ```
437
+
438
+ **Key points:**
439
+ - Externalise the same shared deps as `containers` (React, SL, Radix, ReactQuery, etc.) — the launcher provides them.
440
+ - Capacitor plugins (Tier 2) should be **bundled in**, not externalised — the browser/PWA fallback must work even when the native plugin is absent.
441
+ - Enable/disable the build with `VITE_ENABLE_MOBILE_ADMIN=true` in `.env`.
442
+
443
+ ### Build command
444
+
445
+ ```bash
446
+ # Full pipeline
447
+ vite build && \
448
+ vite build --config vite.config.widget.ts && \
449
+ vite build --config vite.config.container.ts && \
450
+ vite build --config vite.config.mobile-admin.ts
451
+
452
+ # Mobile admin only
453
+ vite build --config vite.config.mobile-admin.ts
454
+ ```
455
+
456
+ ---
457
+
458
+ ## Example: Minimal Mobile Admin Container
459
+
460
+ ```typescript
461
+ // src/mobile-admin/WarehousePickContainer.tsx
462
+ import { useEffect, useState } from 'react'
463
+ import type { AdminMobileHostContext } from '@/lib/admin-mobile-host-context'
464
+
465
+ interface Props {
466
+ host: AdminMobileHostContext
467
+ }
468
+
469
+ export function WarehousePickContainer({ host }: Props) {
470
+ const [lastScan, setLastScan] = useState<string | null>(null)
471
+
472
+ // Auth guard — host may already enforce this, but be explicit
473
+ if (!host.user?.isAdmin) {
474
+ return <p>Admin access required.</p>
475
+ }
476
+
477
+ async function handleScan() {
478
+ try {
479
+ const code = await host.actions.requestQrScan()
480
+ setLastScan(code)
481
+ host.ui.haptic('success')
482
+ } catch (err) {
483
+ host.ui.toast({ title: 'Scan failed', variant: 'destructive' })
484
+ }
485
+ }
486
+
487
+ useEffect(() => {
488
+ host.ui.setHeaderTitle('Warehouse Pick')
489
+ return () => host.ui.setHeaderTitle(null)
490
+ }, [])
491
+
492
+ return (
493
+ <div style={{ padding: 16 }}>
494
+ {host.hardware.qr ? (
495
+ <button onClick={handleScan}>Scan Barcode</button>
496
+ ) : (
497
+ <p>Camera not available on this device</p>
498
+ )}
499
+ {lastScan && <p>Last scan: {lastScan}</p>}
500
+ </div>
501
+ )
502
+ }
503
+ ```
504
+
505
+ ```typescript
506
+ // src/mobile-admin/index.ts
507
+ export { WarehousePickContainer } from './WarehousePickContainer'
508
+
509
+ export const MOBILE_ADMIN_MANIFEST = {
510
+ name: 'WarehousePickContainer',
511
+ version: __APP_VERSION__,
512
+ buildDate: __BUILD_DATE__,
513
+ }
514
+ ```
515
+
516
+ ---
517
+
518
+ ## Best Practices
519
+
520
+ - **Always check `host.hardware.X` before calling `host.actions.X`** — never assume a capability is available.
521
+ - **Always wrap action calls in try/catch** — handle `HostPermissionDeniedError`, `HostTimeoutError`, and `HostCapabilityUnavailableError`.
522
+ - **Use `host.ui.setHeaderTitle` and `host.ui.navigateBack`** for header integration — these are host-only and have no in-container equivalent.
523
+ - **`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.
524
+ - **Use `host.events.subscribe` for lifecycle events** — `'offline'`/`'online'`/`'pause'`/`'resume'` fire consistently on every host.
525
+ - **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.
526
+ - **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
527
+ - **Declare `offline: true`** on a component if it queues writes locally — this signals the launcher to provision offline sync support.
528
+ - **Use `'methodName' in host.actions`** to feature-detect new host capabilities rather than comparing `host._version`. The version is informational only.
@@ -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 |