@proveanything/smartlinks 1.11.1 → 1.11.4
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 +24 -16
- package/dist/docs/app-objects.md +17 -4
- package/dist/docs/mobile-admin-container.md +30 -14
- package/dist/docs/records-admin-pattern.md +305 -334
- package/dist/docs/ui-utils.md +257 -41
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/mobile-admin/errors.d.ts +66 -0
- package/dist/mobile-admin/errors.js +72 -0
- package/dist/mobile-admin/types.d.ts +234 -0
- package/dist/mobile-admin/types.js +1 -0
- package/docs/API_SUMMARY.md +1 -1
- package/docs/app-manifest.md +24 -16
- package/docs/app-objects.md +17 -4
- package/docs/mobile-admin-container.md +30 -14
- package/docs/records-admin-pattern.md +305 -334
- package/docs/ui-utils.md +257 -41
- package/package.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hardware/software capability tokens a mobile admin host may advertise.
|
|
3
|
+
* Passed to `AdminMobileHostContext.capabilities` and to
|
|
4
|
+
* `AdminMobileComponentManifest.capabilities`.
|
|
5
|
+
*/
|
|
6
|
+
export type AdminMobileCapability = 'nfc' | 'nfc-advanced' | 'rfid' | 'qr' | 'camera' | 'keyboard' | 'geolocation' | 'push';
|
|
7
|
+
/**
|
|
8
|
+
* Subset of `AdminMobileCapability` that can be the subject of a structured
|
|
9
|
+
* error. `'keyboard'` is excluded — it is a passive event source with no
|
|
10
|
+
* request method that can fail.
|
|
11
|
+
*/
|
|
12
|
+
export type ActionableCapability = Exclude<AdminMobileCapability, 'keyboard'>;
|
|
13
|
+
/**
|
|
14
|
+
* Canonical identifiers for mobile admin host environments.
|
|
15
|
+
* Use for display / diagnostics only — feature-detect at runtime via
|
|
16
|
+
* `AdminMobileHostContext.capabilities` and `'method' in host.actions`.
|
|
17
|
+
*/
|
|
18
|
+
export type AdminMobileHostId = 'custom-android' | 'capacitor-ios' | 'capacitor-android' | 'pwa' | 'browser';
|
|
19
|
+
/**
|
|
20
|
+
* Discriminated-union of all events the host may emit via
|
|
21
|
+
* `AdminMobileHostContext.events.subscribe`.
|
|
22
|
+
*/
|
|
23
|
+
export type AdminMobileEvent = {
|
|
24
|
+
type: 'nfc-tap';
|
|
25
|
+
uid: string;
|
|
26
|
+
ndef?: string;
|
|
27
|
+
} | {
|
|
28
|
+
type: 'rfid-burst';
|
|
29
|
+
epcs: string[];
|
|
30
|
+
} | {
|
|
31
|
+
type: 'qr-scan';
|
|
32
|
+
code: string;
|
|
33
|
+
} | {
|
|
34
|
+
type: 'key-press';
|
|
35
|
+
keyCode: number;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'lifecycle';
|
|
38
|
+
phase: 'pause' | 'resume' | 'offline' | 'online';
|
|
39
|
+
};
|
|
40
|
+
/** Callback invoked for every hardware event emitted by the host. */
|
|
41
|
+
export type AdminMobileEventCallback = (event: AdminMobileEvent) => void;
|
|
42
|
+
/**
|
|
43
|
+
* The full type of `AdminMobileHostContext.events.subscribe` —
|
|
44
|
+
* takes a callback and returns a cleanup function.
|
|
45
|
+
*/
|
|
46
|
+
export type AdminMobileEventSubscriber = (cb: AdminMobileEventCallback) => () => void;
|
|
47
|
+
/**
|
|
48
|
+
* @deprecated Renamed to `AdminMobileEventCallback` in 1.12.
|
|
49
|
+
* Will be removed in a future minor release.
|
|
50
|
+
* @see AdminMobileEventCallback
|
|
51
|
+
*/
|
|
52
|
+
export type ScannerEventSubscriber = AdminMobileEventCallback;
|
|
53
|
+
/**
|
|
54
|
+
* The `host` prop passed to every mobile admin container component.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* function StockTakeContainer({ host }: { host: AdminMobileHostContext }) {
|
|
58
|
+
* const SL = host.SL; // Always use this — never import the SDK directly
|
|
59
|
+
* return <div>...</div>;
|
|
60
|
+
* }
|
|
61
|
+
*/
|
|
62
|
+
export interface AdminMobileHostContext {
|
|
63
|
+
/** SmartLinks collection this session is scoped to. */
|
|
64
|
+
collectionId: string;
|
|
65
|
+
/** App owning this container. */
|
|
66
|
+
appId: string;
|
|
67
|
+
/**
|
|
68
|
+
* Currently authenticated admin user, or `null` when unauthenticated.
|
|
69
|
+
*/
|
|
70
|
+
user: {
|
|
71
|
+
uid?: string;
|
|
72
|
+
email?: string;
|
|
73
|
+
displayName?: string;
|
|
74
|
+
isAdmin: boolean;
|
|
75
|
+
} | null;
|
|
76
|
+
/**
|
|
77
|
+
* Already-initialised SDK namespace. Containers MUST use this for all API
|
|
78
|
+
* calls — never `import * as SL from '@proveanything/smartlinks'` inside a
|
|
79
|
+
* container, as that would create a second SDK instance.
|
|
80
|
+
*/
|
|
81
|
+
SL: typeof import('../index');
|
|
82
|
+
/** Capabilities advertised by this host instance. */
|
|
83
|
+
capabilities: AdminMobileCapability[];
|
|
84
|
+
/**
|
|
85
|
+
* Static hardware availability flags.
|
|
86
|
+
* These reflect physical capability, not runtime permission state.
|
|
87
|
+
*/
|
|
88
|
+
hardware: {
|
|
89
|
+
nfc: boolean;
|
|
90
|
+
rfid: boolean;
|
|
91
|
+
qr: boolean;
|
|
92
|
+
camera: boolean;
|
|
93
|
+
keyboard: boolean;
|
|
94
|
+
};
|
|
95
|
+
/** Hardware event stream. */
|
|
96
|
+
events: {
|
|
97
|
+
/**
|
|
98
|
+
* Subscribe to hardware events. Returns an unsubscribe function.
|
|
99
|
+
* @param cb - Called for every incoming `AdminMobileEvent`.
|
|
100
|
+
* @returns Cleanup function — call inside `useEffect` return.
|
|
101
|
+
*/
|
|
102
|
+
subscribe: (cb: AdminMobileEventCallback) => () => void;
|
|
103
|
+
};
|
|
104
|
+
/** Imperative hardware actions. */
|
|
105
|
+
actions: {
|
|
106
|
+
/**
|
|
107
|
+
* Open the QR scanner UI and resolve with the decoded string.
|
|
108
|
+
* Throws `HostCapabilityUnavailableError` if `'qr'` is not in `capabilities`.
|
|
109
|
+
* Throws `HostTimeoutError` if the user dismisses without scanning.
|
|
110
|
+
*/
|
|
111
|
+
requestQrScan: () => Promise<string>;
|
|
112
|
+
/**
|
|
113
|
+
* Await the next NFC tap and resolve with uid + optional NDEF payload.
|
|
114
|
+
* @param timeoutMs - Milliseconds before `HostTimeoutError` is thrown (host default if omitted).
|
|
115
|
+
*/
|
|
116
|
+
requestNfcTap: (timeoutMs?: number) => Promise<{
|
|
117
|
+
uid: string;
|
|
118
|
+
ndef?: string;
|
|
119
|
+
}>;
|
|
120
|
+
/**
|
|
121
|
+
* Open the camera shutter once and resolve with the captured `Blob`.
|
|
122
|
+
* Throws `HostCapabilityUnavailableError` if `'camera'` is not in `capabilities`.
|
|
123
|
+
*/
|
|
124
|
+
requestCameraPhoto: () => Promise<Blob>;
|
|
125
|
+
/**
|
|
126
|
+
* Trigger the native share sheet.
|
|
127
|
+
* Falls back to clipboard write on hosts that do not implement the Web Share API.
|
|
128
|
+
*/
|
|
129
|
+
share: (payload: {
|
|
130
|
+
title: string;
|
|
131
|
+
url: string;
|
|
132
|
+
text?: string;
|
|
133
|
+
}) => Promise<void>;
|
|
134
|
+
/** Clipboard access. */
|
|
135
|
+
clipboard: {
|
|
136
|
+
read: () => Promise<string>;
|
|
137
|
+
write: (text: string) => Promise<void>;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Light-touch UI conveniences provided by the host shell.
|
|
142
|
+
* All methods are optional niceties — containers MUST degrade gracefully
|
|
143
|
+
* when absent (e.g. in Storybook or unit tests). Guard every call:
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* host.ui.toast?.({ title: 'Saved' });
|
|
147
|
+
*/
|
|
148
|
+
ui: {
|
|
149
|
+
/** Show a transient status toast. Optional — see interface note. */
|
|
150
|
+
toast?: (opts: {
|
|
151
|
+
title: string;
|
|
152
|
+
description?: string;
|
|
153
|
+
variant?: 'default' | 'destructive';
|
|
154
|
+
}) => void;
|
|
155
|
+
/** Trigger a haptic pulse. Optional — see interface note. */
|
|
156
|
+
haptic?: (style?: 'light' | 'success' | 'error') => void;
|
|
157
|
+
/**
|
|
158
|
+
* Optional — host shell may not provide a managed header
|
|
159
|
+
* (browser tabs, Storybook, desktop views). Guard with `?.`.
|
|
160
|
+
*/
|
|
161
|
+
setHeaderTitle?: (title: string | null) => void;
|
|
162
|
+
/**
|
|
163
|
+
* Optional — host shell may not have a native back stack
|
|
164
|
+
* (browser tabs, Storybook, desktop views). Guard with `?.`.
|
|
165
|
+
*/
|
|
166
|
+
navigateBack?: () => void;
|
|
167
|
+
};
|
|
168
|
+
/** Network connectivity helpers. */
|
|
169
|
+
network: {
|
|
170
|
+
/** Returns whether the device currently has network access. */
|
|
171
|
+
isOnline: () => boolean;
|
|
172
|
+
};
|
|
173
|
+
/** Device information. */
|
|
174
|
+
device: {
|
|
175
|
+
/** Resolves with basic device model/platform metadata. */
|
|
176
|
+
info: () => Promise<{
|
|
177
|
+
model: string;
|
|
178
|
+
platform: string;
|
|
179
|
+
}>;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Host ABI version — informational only.
|
|
183
|
+
* DO NOT branch on this value. Feature-detect at runtime instead:
|
|
184
|
+
* @example
|
|
185
|
+
* if ('requestNfcTap' in host.actions) { ... }
|
|
186
|
+
*/
|
|
187
|
+
_version: number;
|
|
188
|
+
}
|
|
189
|
+
/** Manifest metadata for a single mobile admin container component. */
|
|
190
|
+
export interface AdminMobileComponentManifest {
|
|
191
|
+
/** Component export name (matches the export in the bundle). */
|
|
192
|
+
name: string;
|
|
193
|
+
/** Human-readable description shown in the host launcher UI. */
|
|
194
|
+
description: string;
|
|
195
|
+
/**
|
|
196
|
+
* Hardware capabilities this component requires.
|
|
197
|
+
* The host will hide or disable the component when a required capability
|
|
198
|
+
* is absent from `AdminMobileHostContext.capabilities`.
|
|
199
|
+
*/
|
|
200
|
+
capabilities?: AdminMobileCapability[];
|
|
201
|
+
/**
|
|
202
|
+
* When `true`, the component handles its own offline state and may be
|
|
203
|
+
* launched without network connectivity.
|
|
204
|
+
* @default false
|
|
205
|
+
*/
|
|
206
|
+
offline?: boolean;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Shape of the `mobileAdmin` key inside `app.manifest.json`.
|
|
210
|
+
* Describes the bundle files and the components it exports.
|
|
211
|
+
*/
|
|
212
|
+
export interface AdminMobileBundleManifest {
|
|
213
|
+
files: {
|
|
214
|
+
js: {
|
|
215
|
+
/** UMD bundle path (relative to dist root). Used by the custom-android host. */
|
|
216
|
+
umd: string;
|
|
217
|
+
/** ESM bundle path (relative to dist root). Used by Capacitor/PWA hosts. */
|
|
218
|
+
esm: string;
|
|
219
|
+
};
|
|
220
|
+
/** CSS bundle path, or `null` if the component ships no styles. */
|
|
221
|
+
css: string | null;
|
|
222
|
+
};
|
|
223
|
+
components: AdminMobileComponentManifest[];
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* @deprecated Renamed to `AdminMobileComponentManifest` in 1.12.
|
|
227
|
+
* @see AdminMobileComponentManifest
|
|
228
|
+
*/
|
|
229
|
+
export type MobileAdminComponentManifest = AdminMobileComponentManifest;
|
|
230
|
+
/**
|
|
231
|
+
* @deprecated Renamed to `AdminMobileBundleManifest` in 1.12.
|
|
232
|
+
* @see AdminMobileBundleManifest
|
|
233
|
+
*/
|
|
234
|
+
export type MobileAdminBundleManifest = AdminMobileBundleManifest;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/docs/API_SUMMARY.md
CHANGED
package/docs/app-manifest.md
CHANGED
|
@@ -117,14 +117,18 @@ The manifest is loaded automatically by the platform for every collection page.
|
|
|
117
117
|
|
|
118
118
|
"records": {
|
|
119
119
|
"nutrition": {
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
120
|
+
"label": "Nutrition info",
|
|
121
|
+
"cardinality": "singleton",
|
|
122
|
+
"allowFacetRules": true,
|
|
123
|
+
"scopes": ["collection", "rule", "product", "facet", "batch"],
|
|
124
|
+
"defaultScope": "product"
|
|
123
125
|
},
|
|
124
126
|
"cooking_steps": {
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
127
|
+
"label": "Cooking steps",
|
|
128
|
+
"cardinality": "singleton",
|
|
129
|
+
"allowFacetRules": false,
|
|
130
|
+
"scopes": ["collection", "product"],
|
|
131
|
+
"defaultScope": "product"
|
|
128
132
|
}
|
|
129
133
|
}
|
|
130
134
|
}
|
|
@@ -268,25 +272,29 @@ See the [Deep Link Discovery guide](deep-link-discovery.md) for the full dual-so
|
|
|
268
272
|
|
|
269
273
|
Declares which `app.records` record types the app stores, and which scopes each type supports. Required for any app that follows the [Records-Based Admin Pattern](records-admin-pattern.md). Omit if the app does not use scoped records.
|
|
270
274
|
|
|
271
|
-
The platform and the `<RecordsAdminShell>` from `@proveanything/
|
|
275
|
+
The platform and the `<RecordsAdminShell>` from `@proveanything/smartlinks-utils-ui` read this block to render the right scope tabs, rule editor, and cardinality-appropriate right pane.
|
|
272
276
|
|
|
273
277
|
```json
|
|
274
278
|
"records": {
|
|
275
279
|
"<recordType>": {
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
"
|
|
280
|
+
"label": "Human-readable label",
|
|
281
|
+
"cardinality": "singleton",
|
|
282
|
+
"allowFacetRules": false,
|
|
283
|
+
"scopes": ["collection", "product", "variant", "batch", "facet"],
|
|
284
|
+
"defaultScope": "product"
|
|
279
285
|
}
|
|
280
286
|
}
|
|
281
287
|
```
|
|
282
288
|
|
|
283
|
-
| Field
|
|
284
|
-
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
| `
|
|
289
|
+
| Field | Type | Default | Description |
|
|
290
|
+
|-------------------|----------|---------|-------------|
|
|
291
|
+
| `label` | string | — | Human-readable label for the record type, used in headings and tabs. |
|
|
292
|
+
| `cardinality` | string | `'singleton'` | `'singleton'` — one record wins per scope (e.g. ingredients, nutrition). `'collection'` — every matching record is returned in resolution order (e.g. FAQs, recipes). Drives which hook to use on the public side (`useResolvedRecord` vs `useCollectedRecords`) and how the shell lays out the right pane. |
|
|
293
|
+
| `allowFacetRules` | boolean | `false` | When `true`, the shell renders a **Rule** scope tab and embeds `<FacetRuleEditor>`. Add `'rule'` to `scopes` when setting this. |
|
|
294
|
+
| `scopes` | string[] | — | Allowed scope kinds in resolution order. Valid values: `"collection"`, `"product"`, `"variant"`, `"batch"`, `"facet"`, `"proof"`, `"rule"`. `'rule'` is a synthetic scope holding `facetRule`-targeted records. `'collection'` replaces the legacy empty-ref catch-all — **there is no `'global'` scope**. |
|
|
295
|
+
| `defaultScope` | string | — | The scope the "Create new" button targets in the admin shell. Must be one of the declared `scopes`. |
|
|
288
296
|
|
|
289
|
-
An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`).
|
|
297
|
+
An app may declare multiple record types under different keys (e.g. `"nutrition"` and `"cooking_steps"`). See [records-admin-pattern.md](records-admin-pattern.md) for the full admin + public pattern.
|
|
290
298
|
|
|
291
299
|
#### `executor`
|
|
292
300
|
|
package/docs/app-objects.md
CHANGED
|
@@ -495,7 +495,7 @@ The `ref` field is derived automatically from anchor fields when omitted:
|
|
|
495
495
|
```
|
|
496
496
|
productId: 'prod_abc' → ref: 'product:prod_abc'
|
|
497
497
|
productId: 'prod_abc', variantId: 'var_x' → ref: 'product:prod_abc/variant:var_x'
|
|
498
|
-
(no anchor fields) → ref: '' (
|
|
498
|
+
(no anchor fields) → ref: '' (collection-level catch-all)
|
|
499
499
|
facetRule: { ... } → ref: 'rule:<ulid>'
|
|
500
500
|
```
|
|
501
501
|
|
|
@@ -513,6 +513,20 @@ When multiple scoped records match a context, they are ordered by `specificity`.
|
|
|
513
513
|
| Per `anyOf` value | +1 |
|
|
514
514
|
| No anchors / no rule | 0 |
|
|
515
515
|
|
|
516
|
+
### Resolution order
|
|
517
|
+
|
|
518
|
+
When the public side of a records-based app needs "the data that applies to this product context", the platform walks a canonical chain from most-specific to least-specific:
|
|
519
|
+
|
|
520
|
+
```
|
|
521
|
+
proof → batch → variant → product → rule(*) → facet(*) → collection
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
- `rule(*)` — `facetRule`-targeted records are scored by **specificity** (number of clauses + number of constrained values). The most specific rule wins at its tier.
|
|
525
|
+
- `facet(*)` — legacy single-facet anchors, walked alphabetically. Prefer `facetRule` for new work.
|
|
526
|
+
- `collection` — the top of the chain and the catch-all for any record with no anchor fields. **There is no `'global'` tier above collection.**
|
|
527
|
+
|
|
528
|
+
For a **singleton** record type (one answer wins), use `useResolvedRecord` — it performs this walk server-side and returns the first match plus a `matchedAt` tag. For a **collection** record type (every match is shown), use `useCollectedRecords`. See [records-admin-pattern.md §2](records-admin-pattern.md#2-resolution-order-one-canonical-chain) for the full guide.
|
|
529
|
+
|
|
516
530
|
### Singleton Cardinality
|
|
517
531
|
|
|
518
532
|
By default, `create` always inserts a new row — calling it twice produces two records with identical anchor fields. **Singleton cardinality** changes that: pass `singletonPer` on creation and the server will **upsert** instead, ensuring at most one record of a given `recordType` exists per scope boundary.
|
|
@@ -592,12 +606,11 @@ for (const entry of data) {
|
|
|
592
606
|
case 'product': /* "Inherited from product" */ break;
|
|
593
607
|
case 'facet': /* "Tier-specific" */ break;
|
|
594
608
|
case 'collection': /* "Collection default" */ break;
|
|
595
|
-
case 'universal': /* "Default" */ break;
|
|
596
609
|
}
|
|
597
610
|
}
|
|
598
611
|
```
|
|
599
612
|
|
|
600
|
-
Precedence follows: `
|
|
613
|
+
Precedence follows: `proof > batch > variant > product > rule > facet > collection`. There is no scope above `collection` — a record with no anchor fields is a collection-level catch-all.
|
|
601
614
|
|
|
602
615
|
#### React — `useResolvedRecord`
|
|
603
616
|
|
|
@@ -806,7 +819,7 @@ Examples:
|
|
|
806
819
|
| `productId: 'prod_abc', variantId: 'var_500ml'` | `product:prod_abc/variant:var_500ml` |
|
|
807
820
|
| `batchId: 'batch_q1'` | `batch:batch_q1` |
|
|
808
821
|
| `facetRule: { ... }` | `rule:<ulid>` |
|
|
809
|
-
| *(no anchor fields)* | `''` (
|
|
822
|
+
| *(no anchor fields)* | `''` (collection-level catch-all) |
|
|
810
823
|
|
|
811
824
|
`parseRef` / `buildRef` in `data/refs.ts` should be used for **display and URL round-tripping only**, never as upsert keys. For ETL use cases, set an explicit `ref` using a stable external key (see [External ID / ETL Workflow](#external-id--etl-workflow)).
|
|
812
825
|
|
|
@@ -58,6 +58,8 @@ Your container **never** detects the host directly. It receives a `host` prop fr
|
|
|
58
58
|
|
|
59
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
60
|
|
|
61
|
+
> **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `ActionableCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `AdminMobileEventCallback`, `AdminMobileEventSubscriber`, `AdminMobileComponentManifest`, and `AdminMobileBundleManifest` are all exported from `@proveanything/smartlinks`. Import via `import type { AdminMobileHostContext } from '@proveanything/smartlinks'` — no local mirror needed. `ScannerEventSubscriber`, `MobileAdminComponentManifest`, and `MobileAdminBundleManifest` still export as deprecated aliases.
|
|
62
|
+
|
|
61
63
|
```typescript
|
|
62
64
|
interface AdminMobileHostContext {
|
|
63
65
|
// Identity
|
|
@@ -80,8 +82,8 @@ interface AdminMobileHostContext {
|
|
|
80
82
|
keyboard: boolean
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
// Unified hardware event stream
|
|
84
|
-
events: { subscribe:
|
|
85
|
+
// Unified hardware event stream — callback type: AdminMobileEventCallback
|
|
86
|
+
events: { subscribe: (cb: AdminMobileEventCallback) => () => void }
|
|
85
87
|
|
|
86
88
|
// Promise-based hardware actions — reject with a structured error when unavailable
|
|
87
89
|
actions: {
|
|
@@ -95,12 +97,12 @@ interface AdminMobileHostContext {
|
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
// Host-provided UI helpers
|
|
100
|
+
// Host-provided UI helpers — all optional, see §3 note
|
|
99
101
|
ui: {
|
|
100
|
-
toast
|
|
101
|
-
haptic
|
|
102
|
-
setHeaderTitle
|
|
103
|
-
navigateBack
|
|
102
|
+
toast?: (opts: { title: string; description?: string; variant?: 'default' | 'destructive' }) => void
|
|
103
|
+
haptic?: (style?: 'light' | 'success' | 'error') => void
|
|
104
|
+
setHeaderTitle?: (title: string | null) => void
|
|
105
|
+
navigateBack?: () => void
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
// Network & device info
|
|
@@ -141,9 +143,10 @@ if (host.hardware.nfc) {
|
|
|
141
143
|
|
|
142
144
|
### `host.ui` — native helpers vs. your own components
|
|
143
145
|
|
|
144
|
-
|
|
146
|
+
All four `host.ui` methods are **optional by design**. A container may run in the Sidekick mobile shell, a standalone PWA or browser tab, Storybook, or a screenshot harness — none of those environments is guaranteed to have a native toast system, a managed header, or a back stack. Forcing every host implementer to provide them would exclude the web and desktop use-cases the rest of the contract explicitly supports. Always guard with `?.`:
|
|
145
147
|
|
|
146
|
-
`host.ui.toast()`
|
|
148
|
+
- `host.ui.toast?.({...})` / `host.ui.haptic?.('success')` — optional native feedback. Fall back to your own `<Toaster />` when absent.
|
|
149
|
+
- `host.ui.setHeaderTitle?.('Scanning…')` / `host.ui.navigateBack?.()` — host-shell integrations with no in-container equivalent. Silently no-op when the host doesn't implement them.
|
|
147
150
|
|
|
148
151
|
**Stub pattern for testing and Storybook:**
|
|
149
152
|
|
|
@@ -220,12 +223,23 @@ The custom Kotlin shell and both Capacitor shells ship the same baseline plugin
|
|
|
220
223
|
| `@capacitor/device` | `host.device.info()` |
|
|
221
224
|
| `@capacitor/share` | `host.actions.share()` |
|
|
222
225
|
| `@capacitor/clipboard` | `host.actions.clipboard.*` |
|
|
223
|
-
| `@capacitor/preferences` | `host.storage.*` *(planned)* |
|
|
226
|
+
| `@capacitor/preferences` | `host.storage.*` *(planned — see note below)* |
|
|
224
227
|
| `@capacitor/app` | host-managed (back button, deep links) |
|
|
225
228
|
| `@capacitor/status-bar` | host-managed |
|
|
226
229
|
| `@capacitor/keyboard` | host-managed |
|
|
227
230
|
| `@capacitor/toast` | wired into `host.ui.toast()` |
|
|
228
231
|
|
|
232
|
+
> **`host.storage` — planned shape.** Once released, the surface will wrap `@capacitor/preferences` directly:
|
|
233
|
+
> ```ts
|
|
234
|
+
> host.storage: {
|
|
235
|
+
> get(key: string): Promise<string | null>
|
|
236
|
+
> set(key: string, value: string): Promise<void>
|
|
237
|
+
> remove(key: string): Promise<void>
|
|
238
|
+
> keys(): Promise<string[]>
|
|
239
|
+
> }
|
|
240
|
+
> ```
|
|
241
|
+
> Until then: `localStorage` works on web hosts; use `@capacitor/preferences` directly (bundle it in) on native.
|
|
242
|
+
|
|
229
243
|
### Tier 2 — capability-gated (declare in manifest)
|
|
230
244
|
|
|
231
245
|
| Plugin | Capability flag |
|
|
@@ -383,7 +397,9 @@ try {
|
|
|
383
397
|
| `lifecycle: 'pause'` | App backgrounded | Pause readers to save battery |
|
|
384
398
|
| `lifecycle: 'resume'` | App foregrounded | Resubscribe; refresh stale data |
|
|
385
399
|
|
|
386
|
-
Use `host.events.subscribe` for all lifecycle events —
|
|
400
|
+
Use `host.events.subscribe` for all lifecycle events — all five host types (`custom-android`, `capacitor-ios`, `capacitor-android`, `pwa`, `browser`) are guaranteed to emit every `'pause'`/`'resume'`/`'offline'`/`'online'` phase. No `window.addEventListener('online')` fallback is needed.
|
|
401
|
+
|
|
402
|
+
> **Planned** — `phase: 'mount' | 'unmount'` events are on the roadmap. These will fire when the container becomes visible / is removed from the host view stack, enabling deferred reader start-up without a `useEffect` dependency.
|
|
387
403
|
|
|
388
404
|
---
|
|
389
405
|
|
|
@@ -460,7 +476,7 @@ vite build --config vite.config.mobile-admin.ts
|
|
|
460
476
|
```typescript
|
|
461
477
|
// src/mobile-admin/WarehousePickContainer.tsx
|
|
462
478
|
import { useEffect, useState } from 'react'
|
|
463
|
-
import type { AdminMobileHostContext } from '
|
|
479
|
+
import type { AdminMobileHostContext } from '@proveanything/smartlinks'
|
|
464
480
|
|
|
465
481
|
interface Props {
|
|
466
482
|
host: AdminMobileHostContext
|
|
@@ -519,8 +535,8 @@ export const MOBILE_ADMIN_MANIFEST = {
|
|
|
519
535
|
|
|
520
536
|
- **Always check `host.hardware.X` before calling `host.actions.X`** — never assume a capability is available.
|
|
521
537
|
- **Always wrap action calls in try/catch** — handle `HostPermissionDeniedError`, `HostTimeoutError`, and `HostCapabilityUnavailableError`.
|
|
522
|
-
- **
|
|
523
|
-
- **`host.ui.
|
|
538
|
+
- **All four `host.ui` methods are optional** (`toast`, `haptic`, `setHeaderTitle`, `navigateBack`) — guard every call with `?.`. See the `host.ui` section above.
|
|
539
|
+
- **`host.ui.setHeaderTitle` and `host.ui.navigateBack`** integrate with the host shell and have no in-container equivalent; call with `?.`.
|
|
524
540
|
- **Use `host.events.subscribe` for lifecycle events** — `'offline'`/`'online'`/`'pause'`/`'resume'` fire consistently on every host.
|
|
525
541
|
- **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
542
|
- **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
|