@proveanything/smartlinks 1.10.3 → 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.
- 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 +467 -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 +467 -0
- package/docs/overview.md +12 -8
- package/package.json +1 -1
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
|
+
"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.
|
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,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/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 |
|
package/docs/API_SUMMARY.md
CHANGED
package/docs/app-manifest.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
|
+
"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.
|
package/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,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
|
|
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 |
|