@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.
- package/dist/docs/API_SUMMARY.md +1 -1
- package/dist/docs/app-manifest.md +63 -0
- package/dist/docs/containers.md +23 -0
- package/dist/docs/mobile-admin-container.md +528 -0
- package/dist/docs/overview.md +12 -8
- package/docs/API_SUMMARY.md +1 -1
- package/docs/app-manifest.md +63 -0
- package/docs/containers.md +23 -0
- package/docs/mobile-admin-container.md +528 -0
- package/docs/overview.md +12 -8
- package/package.json +1 -1
- package/docs/scanner-container.md +0 -556
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -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.
|
package/dist/docs/containers.md
CHANGED
|
@@ -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.
|
package/dist/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
|
|
26
|
+
Each microapp can be consumed in one or more of the following ways:
|
|
27
27
|
|
|
28
|
-
| Mode |
|
|
29
|
-
|
|
30
|
-
| **
|
|
31
|
-
| **
|
|
32
|
-
| **
|
|
33
|
-
| **
|
|
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
|
|
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 |
|