@proveanything/smartlinks 1.11.4 → 1.11.6
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-objects.md +56 -0
- package/dist/docs/mobile-admin-container.md +30 -2
- package/dist/docs/native-facade.md +170 -0
- package/dist/index.d.ts +1 -0
- package/dist/mobile-admin/types.d.ts +11 -0
- package/dist/native/types.d.ts +228 -0
- package/dist/native/types.js +15 -0
- package/docs/API_SUMMARY.md +1 -1
- package/docs/app-objects.md +56 -0
- package/docs/mobile-admin-container.md +30 -2
- package/docs/native-facade.md +170 -0
- package/package.json +1 -1
package/dist/docs/API_SUMMARY.md
CHANGED
package/dist/docs/app-objects.md
CHANGED
|
@@ -85,6 +85,7 @@ Zones are **automatically filtered** based on the caller's role:
|
|
|
85
85
|
### Zone Writing Rules
|
|
86
86
|
|
|
87
87
|
- **Non-admin callers** attempting to write to the `admin` zone are silently ignored
|
|
88
|
+
- **Authenticated record owners** can write to `data` and `owner` by default; individual keys can be restricted via the `ownerEdit` app config policy (see [Owner Edit Policy](#owner-edit-policy) below)
|
|
88
89
|
- **Public callers** can write to `data` and `owner` (if visibility allows)
|
|
89
90
|
- **Admins** can write to all three zones
|
|
90
91
|
|
|
@@ -1098,6 +1099,61 @@ The `enforce` values are **merged over** the caller's request body, so you can l
|
|
|
1098
1099
|
|
|
1099
1100
|
---
|
|
1100
1101
|
|
|
1102
|
+
## Owner Edit Policy
|
|
1103
|
+
|
|
1104
|
+
Gives per-zone, field-level control over what an **authenticated record owner** can update via `PATCH /api/v1/public/collection/:collectionId/app/:appId/records/:recordId`.
|
|
1105
|
+
|
|
1106
|
+
Set the policy in the same app config document used for `publicCreate` (stored at `sites/{collectionId}/apps/{appId}`):
|
|
1107
|
+
|
|
1108
|
+
```json
|
|
1109
|
+
{
|
|
1110
|
+
"ownerEdit": {
|
|
1111
|
+
"records": {
|
|
1112
|
+
"data": { "allow": ["paypalEmail"] },
|
|
1113
|
+
"owner": { "allow": ["paypalEmail", "paypalEmailUpdatedAt"] }
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
### Zone visibility and write access
|
|
1120
|
+
|
|
1121
|
+
| Zone | Who can read | Who can write (owner) |
|
|
1122
|
+
|---------|------------------------|----------------------------------------------------------|
|
|
1123
|
+
| `data` | public | Allow-listed keys only (if policy set); all keys if not |
|
|
1124
|
+
| `owner` | owner + admin | Allow-listed keys only (if policy set); all keys if not |
|
|
1125
|
+
| `admin` | admin | Never — admin zone is always immutable to owners |
|
|
1126
|
+
|
|
1127
|
+
### Allow-list semantics
|
|
1128
|
+
|
|
1129
|
+
| Config | Behaviour |
|
|
1130
|
+
|----------------------------|-------------------------------------------------------------------------------|
|
|
1131
|
+
| No `ownerEdit` key | Default-allow — both zones fully writable (no change to existing behaviour) |
|
|
1132
|
+
| `allow` array with keys | Only the listed keys are accepted from the PATCH body; the rest are silently ignored and their existing values preserved |
|
|
1133
|
+
| `allow: []` (empty array) | Zone is effectively read-only for the owner |
|
|
1134
|
+
|
|
1135
|
+
Accepted keys are **merged** onto the existing zone blob — you do not need to re-send unchanged values.
|
|
1136
|
+
|
|
1137
|
+
### Example: commission record with protected fields
|
|
1138
|
+
|
|
1139
|
+
An app that lets owners update their payout email but not their commission total:
|
|
1140
|
+
|
|
1141
|
+
```json
|
|
1142
|
+
{
|
|
1143
|
+
"ownerEdit": {
|
|
1144
|
+
"records": {
|
|
1145
|
+
"owner": { "allow": ["paypalEmail", "paypalEmailUpdatedAt"] }
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
A PATCH body of `{ "owner": { "paypalEmail": "x@y.com", "totalCommission": 99 } }` will update `paypalEmail` only. `totalCommission` is silently ignored and its existing value is preserved.
|
|
1152
|
+
|
|
1153
|
+
> **App design note:** If your app creates records with sensitive fields that owners should never modify (e.g. computed totals, server-assigned fields), add an `ownerEdit` policy from the start. It is significantly easier to relax restrictions later than to tighten them after data has been mutated.
|
|
1154
|
+
|
|
1155
|
+
---
|
|
1156
|
+
|
|
1101
1157
|
## Anonymous Edit Tokens
|
|
1102
1158
|
|
|
1103
1159
|
Enables an anonymous caller to amend a record they just created — without authentication — by presenting a short-lived secret token.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
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
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.
|
|
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. For the full `host.native` facade contract (share, clipboard, haptics, NFC, RFID, storage, etc.) see [native-facade.md](native-facade.md).
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -23,6 +23,7 @@ This document describes how to build a **Mobile Admin Container** — a SmartLin
|
|
|
23
23
|
11. [Build & Bundle Requirements](#build--bundle-requirements)
|
|
24
24
|
12. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
|
|
25
25
|
13. [Best Practices](#best-practices)
|
|
26
|
+
14. [Native Capability Facade](#native-capability-facade)
|
|
26
27
|
|
|
27
28
|
---
|
|
28
29
|
|
|
@@ -58,7 +59,7 @@ Your container **never** detects the host directly. It receives a `host` prop fr
|
|
|
58
59
|
|
|
59
60
|
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
|
|
|
61
|
-
> **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `ActionableCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `AdminMobileEventCallback`, `AdminMobileEventSubscriber`, `AdminMobileComponentManifest`, and `
|
|
62
|
+
> **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `ActionableCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `AdminMobileEventCallback`, `AdminMobileEventSubscriber`, `AdminMobileComponentManifest`, `AdminMobileBundleManifest`, and `NativeFacade` (plus all sub-facade interfaces) are 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
63
|
|
|
63
64
|
```typescript
|
|
64
65
|
interface AdminMobileHostContext {
|
|
@@ -112,6 +113,10 @@ interface AdminMobileHostContext {
|
|
|
112
113
|
// Informational host version — use for logging/diagnostics, not feature detection.
|
|
113
114
|
// For feature detection prefer existence checks: 'requestNfcTap' in host.actions
|
|
114
115
|
_version: number
|
|
116
|
+
|
|
117
|
+
// Full native capability facade — share, clipboard, haptics, NFC, RFID, storage, etc.
|
|
118
|
+
// Optional: not every host stub populates it. See native-facade.md.
|
|
119
|
+
native?: NativeFacade
|
|
115
120
|
}
|
|
116
121
|
```
|
|
117
122
|
|
|
@@ -542,3 +547,26 @@ export const MOBILE_ADMIN_MANIFEST = {
|
|
|
542
547
|
- **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
|
|
543
548
|
- **Declare `offline: true`** on a component if it queues writes locally — this signals the launcher to provision offline sync support.
|
|
544
549
|
- **Use `'methodName' in host.actions`** to feature-detect new host capabilities rather than comparing `host._version`. The version is informational only.
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Native Capability Facade
|
|
554
|
+
|
|
555
|
+
`host.native` gives containers access to a broader set of device capabilities — share sheet, clipboard, full haptics API, storage, QR, NFC, RFID (Kotlin only), auth, and cross-shell events — through a single interface that falls back gracefully across all host environments.
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
// Check host.hardware.* for physical availability, then call via host.native
|
|
559
|
+
if (host.hardware.nfc && host.native?.nfc) {
|
|
560
|
+
const { uid } = await host.native.nfc.read({ timeoutMs: 10_000 })
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Storage always available (Preferences → localStorage → in-memory Map)
|
|
564
|
+
await host.native?.storage.set('lastScan', uid)
|
|
565
|
+
|
|
566
|
+
// Share sheet with web fallback
|
|
567
|
+
await host.native?.share.share({ title: 'Found tag', url: `https://app.example/tags/${uid}` })
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
`host.native` is optional on `AdminMobileHostContext` — host stubs (Storybook, unit tests) need not implement it. `native.rfid` is additionally optional within `NativeFacade` itself, as it only exists on `custom-android`.
|
|
571
|
+
|
|
572
|
+
For the full sub-facade table, fallback chains, and what's intentionally not wrapped, see **[native-facade.md](native-facade.md)**.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Native Capability Facade (`host.native` / `SL.native`)
|
|
2
|
+
|
|
3
|
+
> **Version:** 1.12 · **Platform:** SmartLinks R4 · **Last updated:** 2026-04-30
|
|
4
|
+
|
|
5
|
+
The `NativeFacade` is a thin contract layer between microapps and the device capabilities available on the current host shell (Kotlin, Capacitor iOS/Android, PWA, or browser). It lets a microapp call `host.native.share.share({...})` without knowing whether it's running over `window.SmartlinksScanner`, a Capacitor plugin, or `navigator.share`.
|
|
6
|
+
|
|
7
|
+
**The SDK owns the contract** (`src/native/types.ts`, re-exported from `@proveanything/smartlinks`). Each host implementation is responsible for wiring up the sub-facades and populating:
|
|
8
|
+
- `AdminMobileHostContext.native` (container prop, typed in the SDK), and
|
|
9
|
+
- `window.SL.native` (for UMD microapps that don't receive a host prop — host concern, not SDK).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
1. [Access patterns](#access-patterns)
|
|
16
|
+
2. [What's in the facade](#whats-in-the-facade)
|
|
17
|
+
3. [What's NOT in the facade](#whats-not-in-the-facade)
|
|
18
|
+
4. [Error contract](#error-contract)
|
|
19
|
+
5. [Type reference](#type-reference)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Access patterns
|
|
24
|
+
|
|
25
|
+
### Inside a Mobile Admin Container (recommended)
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import type { AdminMobileHostContext } from '@proveanything/smartlinks'
|
|
29
|
+
|
|
30
|
+
function MyContainer({ host }: { host: AdminMobileHostContext }) {
|
|
31
|
+
const handleShare = async () => {
|
|
32
|
+
await host.native?.share.share({ title: 'SmartLinks', url: window.location.href })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleScan = async () => {
|
|
36
|
+
// Only present when the host provides a QR reader
|
|
37
|
+
const code = await host.native?.qr.scan()
|
|
38
|
+
if (code) processCode(code)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Always call via `host.native?.<facade>.<method>()` — `native` is optional because not every host exposes the full surface (e.g. a minimal browser stub may omit `rfid`).
|
|
44
|
+
|
|
45
|
+
For hardware-gated capabilities (NFC, RFID, QR, camera) also check `host.capabilities` or `host.hardware.*` before calling, as those flags reflect physical availability on the current device:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
if (host.hardware.nfc && host.native?.nfc) {
|
|
49
|
+
const { uid } = await host.native.nfc.read({ timeoutMs: 10_000 })
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Inside a UMD microapp (no host prop)
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// window.SL.native is populated by the host at boot — host concern, not SDK
|
|
57
|
+
const native = (window as any).SL?.native as import('@proveanything/smartlinks').NativeFacade | undefined
|
|
58
|
+
const code = await native?.qr.scan()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## What's in the facade
|
|
64
|
+
|
|
65
|
+
These capabilities have meaningful fallbacks across Kotlin / Capacitor / web. Always go through the facade.
|
|
66
|
+
|
|
67
|
+
| Sub-facade | Key methods | Fallback chain |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `native.share` | `share({ title, url, text? })`, `canShare()` | Capacitor Share → `navigator.share` → copy to clipboard + toast |
|
|
70
|
+
| `native.clipboard` | `write(text)`, `read()` | Capacitor Clipboard → `navigator.clipboard` → `execCommand('copy')` |
|
|
71
|
+
| `native.haptics` | `impact(style?)`, `notification(style)`, `selection()` | Capacitor Haptics → `navigator.vibrate` → no-op |
|
|
72
|
+
| `native.network` | `getStatus()`, `addListener('change', cb)` | Capacitor Network → `navigator.onLine` + `'online'`/`'offline'` events |
|
|
73
|
+
| `native.device` | `getInfo()`, `getId()`, `getLanguageCode()` | Capacitor Device → UA parsing + cached UUID in `localStorage` |
|
|
74
|
+
| `native.storage` | `get(key)`, `set(key, value)`, `remove(key)`, `keys()` | Capacitor Preferences → `localStorage` → in-memory `Map` (private mode) |
|
|
75
|
+
| `native.qr` | `scan(opts?)` | Capacitor MLKit Barcode → html5-qrcode / `BarcodeDetector` |
|
|
76
|
+
| `native.auth` | `signInWithGoogle()`, `signOut()` | Kotlin bridge → Capacitor Browser + OAuth → web redirect |
|
|
77
|
+
| `native.nfc` | `read(opts?)`, `writeNdef(opts)`, `programTag(opts)`, `lockTag(opts)`, `isLocked(opts)` | Kotlin bridge → Web NFC (`NDEFReader`) → `HostCapabilityUnavailableError` |
|
|
78
|
+
| `native.rfid` | `startScan(opts?)`, `stopScan()`, `subscribe(cb)` | Kotlin bridge only — throws `HostCapabilityUnavailableError` on all other hosts |
|
|
79
|
+
| `native.events` | `subscribe(type, cb)`, `emit(type, payload?)` | Internal pub/sub bridging Kotlin `onSmartlinksData` + Capacitor listeners |
|
|
80
|
+
| `native.webSource` | `get()`, `set({ mode, channel?, liveUrl? })` | Kotlin bridge + Capacitor Preferences mirror |
|
|
81
|
+
|
|
82
|
+
> **`native.rfid` is the only optional sub-facade** on `NativeFacade` — it is only populated on `custom-android` hosts. All other sub-facades are present on every host, though individual methods may throw `HostCapabilityUnavailableError` when the underlying capability is absent.
|
|
83
|
+
|
|
84
|
+
> **`native.device` vs `host.device`** — `host.device.info()` is a simplified convenience that returns `{ model, platform }`. `host.native?.device.getInfo()` returns the full `DeviceInfo` (adds `osVersion`, `manufacturer`, `isVirtual`) and also provides `getId()` and `getLanguageCode()`. Use `host.device` when the convenience is enough.
|
|
85
|
+
|
|
86
|
+
> **`native.network` vs `host.network`** — `host.network.isOnline()` is a boolean convenience. `host.native?.network.getStatus()` returns `{ connected, connectionType }` including `'wifi'` / `'cellular'` distinction.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## What's NOT in the facade
|
|
91
|
+
|
|
92
|
+
These capabilities intentionally have no `SL.native` wrapper. Call the Capacitor plugin directly, or use the web API. The host provides them as Tier 1 or Tier 2 plugins (see [Capacitor Plugin Baseline](mobile-admin-container.md#capacitor-plugin-baseline)).
|
|
93
|
+
|
|
94
|
+
| Capability | Why not wrapped |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `@capacitor/push-notifications` | Push only exists in Capacitor builds; no cross-shell registration flow. Import and call directly; detect Capacitor first. |
|
|
97
|
+
| `@capacitor/local-notifications` | Same — no Kotlin or web equivalent worth abstracting. |
|
|
98
|
+
| `@capacitor/camera` | Plugin already abstracts iOS/Android. PWA's `<input capture>` is too different in shape to unify. |
|
|
99
|
+
| `@capacitor/filesystem` | No web equivalent (storage quotas, sandbox rules differ). Use `URL.createObjectURL` + download anchor on web. |
|
|
100
|
+
| `@capacitor/geolocation` | `navigator.geolocation` already works in WebViews; the Capacitor plugin wraps the same OS APIs. No facade adds value. |
|
|
101
|
+
| `@capacitor/keyboard`, `@capacitor/screen-*`, `@capacitor/splash-screen`, `@capacitor/status-bar`, `@capacitor/dialog`, `@capacitor/app` | Per-host boot-time config or platform-chrome concerns; not part of the microapp contract. |
|
|
102
|
+
| `@capacitor/browser` | In-app browser for OAuth is intentionally Capacitor-only. For opening external URLs use `native.share.share({ url })`. |
|
|
103
|
+
| `@capawesome/capacitor-file-picker` | Picker UX too divergent across web (`<input type="file">`) and native to wrap usefully. |
|
|
104
|
+
|
|
105
|
+
If a capability later needs consistent behaviour across shells (e.g. notifications reach enough hosts to warrant throttling / routing via the facade), promote it into `NativeFacade` by adding an interface to `src/native/types.ts` and a row to the table above.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Error contract
|
|
110
|
+
|
|
111
|
+
All facade methods reject with one of the three structured errors from `@proveanything/smartlinks`. A single catch block handles every capability and host combination:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import {
|
|
115
|
+
HostCapabilityUnavailableError,
|
|
116
|
+
HostPermissionDeniedError,
|
|
117
|
+
HostTimeoutError,
|
|
118
|
+
} from '@proveanything/smartlinks'
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const { uid } = await host.native?.nfc.read({ timeoutMs: 10_000 }) ?? {}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err instanceof HostCapabilityUnavailableError) {
|
|
124
|
+
// host.capabilities won't list 'nfc' — we should have checked first
|
|
125
|
+
host.ui.toast?.({ title: 'NFC not available on this device', variant: 'destructive' })
|
|
126
|
+
} else if (err instanceof HostPermissionDeniedError) {
|
|
127
|
+
host.ui.toast?.({ title: 'NFC permission denied', variant: 'destructive' })
|
|
128
|
+
} else if (err instanceof HostTimeoutError) {
|
|
129
|
+
host.ui.toast?.({ title: `No tag detected after ${err.timeoutMs / 1000}s` })
|
|
130
|
+
} else {
|
|
131
|
+
throw err
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Type reference
|
|
139
|
+
|
|
140
|
+
All types below are exported from `@proveanything/smartlinks`:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import type {
|
|
144
|
+
NativeFacade, // root — use on AdminMobileHostContext.native
|
|
145
|
+
NativeCapability, // union of sub-facade keys: 'share' | 'clipboard' | ...
|
|
146
|
+
ShareFacade,
|
|
147
|
+
ClipboardFacade,
|
|
148
|
+
HapticsFacade,
|
|
149
|
+
HapticImpactStyle, // 'light' | 'medium' | 'heavy'
|
|
150
|
+
HapticNotificationStyle,
|
|
151
|
+
NetworkFacade,
|
|
152
|
+
NetworkStatus,
|
|
153
|
+
DeviceFacade,
|
|
154
|
+
DeviceInfo,
|
|
155
|
+
StorageFacade,
|
|
156
|
+
QrFacade,
|
|
157
|
+
QrScanOptions,
|
|
158
|
+
AuthFacade,
|
|
159
|
+
NfcFacade,
|
|
160
|
+
NfcReadResult,
|
|
161
|
+
RfidFacade,
|
|
162
|
+
RfidScanOptions,
|
|
163
|
+
EventsFacade,
|
|
164
|
+
WebSourceFacade,
|
|
165
|
+
WebSourceMode,
|
|
166
|
+
WebSourceConfig,
|
|
167
|
+
} from '@proveanything/smartlinks'
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The facade interfaces are contract-only — no runtime implementation is shipped in the SDK. You only need them for TypeScript type-checking (authoring containers or host stubs).
|
package/dist/index.d.ts
CHANGED
|
@@ -27,3 +27,4 @@ export type { AdminMobileCapability, ActionableCapability, AdminMobileHostId, Ad
|
|
|
27
27
|
AdminMobileHostContext, AdminMobileComponentManifest, AdminMobileBundleManifest, MobileAdminComponentManifest, // @deprecated — use AdminMobileComponentManifest
|
|
28
28
|
MobileAdminBundleManifest, } from './mobile-admin/types';
|
|
29
29
|
export { HostCapabilityUnavailableError, HostPermissionDeniedError, HostTimeoutError, } from './mobile-admin/errors';
|
|
30
|
+
export type { NativeCapability, NativeFacade, ShareFacade, ClipboardFacade, HapticImpactStyle, HapticNotificationStyle, HapticsFacade, NetworkStatus, NetworkFacade, DeviceInfo, DeviceFacade, StorageFacade, QrScanOptions, QrFacade, AuthFacade, NfcReadResult, NfcFacade, RfidScanOptions, RfidFacade, EventsFacade, WebSourceMode, WebSourceConfig, WebSourceFacade, } from './native/types';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NativeFacade } from '../native/types';
|
|
1
2
|
/**
|
|
2
3
|
* Hardware/software capability tokens a mobile admin host may advertise.
|
|
3
4
|
* Passed to `AdminMobileHostContext.capabilities` and to
|
|
@@ -185,6 +186,16 @@ export interface AdminMobileHostContext {
|
|
|
185
186
|
* if ('requestNfcTap' in host.actions) { ... }
|
|
186
187
|
*/
|
|
187
188
|
_version: number;
|
|
189
|
+
/**
|
|
190
|
+
* Full native capability facade populated by the host at mount time.
|
|
191
|
+
* Not all sub-facades are present on every host — check before calling:
|
|
192
|
+
* @example
|
|
193
|
+
* const code = await host.native?.qr.scan();
|
|
194
|
+
* const { identifier } = await host.native?.device.getId() ?? {};
|
|
195
|
+
* For UMD microapps that do not receive a `host` prop, the same object
|
|
196
|
+
* is available at `window.SL.native` (host concern, not SDK).
|
|
197
|
+
*/
|
|
198
|
+
native?: NativeFacade;
|
|
188
199
|
}
|
|
189
200
|
/** Manifest metadata for a single mobile admin container component. */
|
|
190
201
|
export interface AdminMobileComponentManifest {
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract types for the `SL.native` / `host.native` facade.
|
|
3
|
+
*
|
|
4
|
+
* These are **interface-only** — no runtime implementation lives here.
|
|
5
|
+
* The facade is implemented per-host (Sidekick, Capacitor builds, PWA) and
|
|
6
|
+
* exposed to containers via `AdminMobileHostContext.native` or (for UMD
|
|
7
|
+
* microapps) via `window.SL.native`. The SDK owns the shape; the host owns
|
|
8
|
+
* the wiring.
|
|
9
|
+
*
|
|
10
|
+
* All facade methods reject with one of the structured errors from
|
|
11
|
+
* `@proveanything/smartlinks` (`HostCapabilityUnavailableError`,
|
|
12
|
+
* `HostPermissionDeniedError`, `HostTimeoutError`) so a single catch handler
|
|
13
|
+
* covers every host and capability.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Keys of the `NativeFacade` interface — one token per sub-facade.
|
|
17
|
+
* Use `hasCapability(key)` on the host to check availability before calling.
|
|
18
|
+
*/
|
|
19
|
+
export type NativeCapability = 'share' | 'clipboard' | 'haptics' | 'network' | 'device' | 'storage' | 'qr' | 'auth' | 'nfc' | 'rfid' | 'events' | 'webSource';
|
|
20
|
+
/** Share sheet / URL sharing. */
|
|
21
|
+
export interface ShareFacade {
|
|
22
|
+
/**
|
|
23
|
+
* Trigger the native share sheet.
|
|
24
|
+
* Falls back to `navigator.share`, then silently copies `url` to clipboard.
|
|
25
|
+
*/
|
|
26
|
+
share(payload: {
|
|
27
|
+
title: string;
|
|
28
|
+
url: string;
|
|
29
|
+
text?: string;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
/** Returns `true` when a share sheet is available on this host. */
|
|
32
|
+
canShare(): Promise<boolean>;
|
|
33
|
+
}
|
|
34
|
+
/** Clipboard read / write. */
|
|
35
|
+
export interface ClipboardFacade {
|
|
36
|
+
/** Write plain text to the clipboard. */
|
|
37
|
+
write(text: string): Promise<void>;
|
|
38
|
+
/** Read plain text from the clipboard. Returns `''` if empty or denied. */
|
|
39
|
+
read(): Promise<string>;
|
|
40
|
+
}
|
|
41
|
+
/** Haptic feedback. */
|
|
42
|
+
export type HapticImpactStyle = 'light' | 'medium' | 'heavy';
|
|
43
|
+
export type HapticNotificationStyle = 'success' | 'warning' | 'error';
|
|
44
|
+
export interface HapticsFacade {
|
|
45
|
+
/** Physical impact pulse. Falls back to `navigator.vibrate`. */
|
|
46
|
+
impact(style?: HapticImpactStyle): Promise<void>;
|
|
47
|
+
/** Notification-style feedback. Falls back to `navigator.vibrate`. */
|
|
48
|
+
notification(style: HapticNotificationStyle): Promise<void>;
|
|
49
|
+
/** Selection-changed feedback. No-op when unsupported. */
|
|
50
|
+
selection(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/** Network status. */
|
|
53
|
+
export interface NetworkStatus {
|
|
54
|
+
connected: boolean;
|
|
55
|
+
connectionType: 'wifi' | 'cellular' | 'none' | 'unknown';
|
|
56
|
+
}
|
|
57
|
+
export interface NetworkFacade {
|
|
58
|
+
/** Returns current connection status. */
|
|
59
|
+
getStatus(): Promise<NetworkStatus>;
|
|
60
|
+
/**
|
|
61
|
+
* Subscribe to connectivity changes. Returns an unsubscribe function.
|
|
62
|
+
* Falls back to `window` `'online'`/`'offline'` events on web.
|
|
63
|
+
*/
|
|
64
|
+
addListener(event: 'change', cb: (status: NetworkStatus) => void): () => void;
|
|
65
|
+
}
|
|
66
|
+
/** Device metadata. */
|
|
67
|
+
export interface DeviceInfo {
|
|
68
|
+
model: string;
|
|
69
|
+
platform: 'ios' | 'android' | 'web';
|
|
70
|
+
osVersion: string;
|
|
71
|
+
manufacturer: string;
|
|
72
|
+
isVirtual: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface DeviceFacade {
|
|
75
|
+
/** Hardware and OS metadata. */
|
|
76
|
+
getInfo(): Promise<DeviceInfo>;
|
|
77
|
+
/**
|
|
78
|
+
* Stable device identifier — UUID persisted in `@capacitor/preferences`
|
|
79
|
+
* (native) or `localStorage` (web).
|
|
80
|
+
*/
|
|
81
|
+
getId(): Promise<{
|
|
82
|
+
identifier: string;
|
|
83
|
+
}>;
|
|
84
|
+
/** Current locale language code, e.g. `'en'`, `'fr'`. */
|
|
85
|
+
getLanguageCode(): Promise<{
|
|
86
|
+
value: string;
|
|
87
|
+
}>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Persistent key-value storage.
|
|
91
|
+
* Backed by `@capacitor/preferences` on native; `localStorage` on web;
|
|
92
|
+
* in-memory `Map` in private-browsing / third-party-cookie contexts.
|
|
93
|
+
*/
|
|
94
|
+
export interface StorageFacade {
|
|
95
|
+
get(key: string): Promise<string | null>;
|
|
96
|
+
set(key: string, value: string): Promise<void>;
|
|
97
|
+
remove(key: string): Promise<void>;
|
|
98
|
+
keys(): Promise<string[]>;
|
|
99
|
+
}
|
|
100
|
+
/** QR / barcode scan. */
|
|
101
|
+
export interface QrScanOptions {
|
|
102
|
+
/** BarcodeFormat strings to restrict scanning (e.g. `['QR_CODE', 'EAN_13']`). */
|
|
103
|
+
formats?: string[];
|
|
104
|
+
}
|
|
105
|
+
export interface QrFacade {
|
|
106
|
+
/**
|
|
107
|
+
* Open the barcode scanner UI and resolve with the decoded string.
|
|
108
|
+
* Uses Capacitor MLKit on native; html5-qrcode (or `BarcodeDetector`) on web.
|
|
109
|
+
* Throws `HostTimeoutError` when the user dismisses without scanning.
|
|
110
|
+
*/
|
|
111
|
+
scan(opts?: QrScanOptions): Promise<string>;
|
|
112
|
+
}
|
|
113
|
+
/** Authentication. */
|
|
114
|
+
export interface AuthFacade {
|
|
115
|
+
/**
|
|
116
|
+
* Initiate Google Sign-In. Returns the raw ID token for exchange with the
|
|
117
|
+
* SmartLinks auth backend. Falls back to an OAuth redirect flow on web.
|
|
118
|
+
*/
|
|
119
|
+
signInWithGoogle(): Promise<{
|
|
120
|
+
idToken: string;
|
|
121
|
+
}>;
|
|
122
|
+
signOut(): Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
/** NFC tag operations. */
|
|
125
|
+
export interface NfcReadResult {
|
|
126
|
+
uid: string;
|
|
127
|
+
/** Raw NDEF payload (hex or base64 depending on host), when present. */
|
|
128
|
+
ndef?: string;
|
|
129
|
+
}
|
|
130
|
+
export interface NfcFacade {
|
|
131
|
+
/**
|
|
132
|
+
* Wait for the next NFC tap and return the tag's UID + optional NDEF.
|
|
133
|
+
* Throws `HostTimeoutError` after `opts.timeoutMs` (default: 30 s).
|
|
134
|
+
*/
|
|
135
|
+
read(opts?: {
|
|
136
|
+
timeoutMs?: number;
|
|
137
|
+
}): Promise<NfcReadResult>;
|
|
138
|
+
/** Write an NDEF record to a tag by UID. */
|
|
139
|
+
writeNdef(opts: {
|
|
140
|
+
uid: string;
|
|
141
|
+
payload: string;
|
|
142
|
+
}): Promise<void>;
|
|
143
|
+
/** Program an NTAG 21x / 424 profile. Host-specific; Kotlin only on advanced ops. */
|
|
144
|
+
programTag(opts: {
|
|
145
|
+
uid: string;
|
|
146
|
+
profile: string;
|
|
147
|
+
[key: string]: unknown;
|
|
148
|
+
}): Promise<void>;
|
|
149
|
+
/** Lock a tag against further writes. */
|
|
150
|
+
lockTag(opts: {
|
|
151
|
+
uid: string;
|
|
152
|
+
}): Promise<void>;
|
|
153
|
+
/** Returns whether the tag is currently locked. */
|
|
154
|
+
isLocked(opts: {
|
|
155
|
+
uid: string;
|
|
156
|
+
}): Promise<boolean>;
|
|
157
|
+
}
|
|
158
|
+
/** RFID mass-scan (UHF). Only available on `custom-android`. */
|
|
159
|
+
export interface RfidScanOptions {
|
|
160
|
+
/** Transmission power in dBm. */
|
|
161
|
+
power?: number;
|
|
162
|
+
/** Gen2 session flag (0–3). */
|
|
163
|
+
sessionFlag?: number;
|
|
164
|
+
}
|
|
165
|
+
export interface RfidFacade {
|
|
166
|
+
/** Start the RFID reader. Throws `HostCapabilityUnavailableError` on non-Kotlin hosts. */
|
|
167
|
+
startScan(opts?: RfidScanOptions): Promise<void>;
|
|
168
|
+
stopScan(): Promise<void>;
|
|
169
|
+
/** Subscribe to EPC bursts. Returns an unsubscribe function. */
|
|
170
|
+
subscribe(cb: (epcs: string[]) => void): () => void;
|
|
171
|
+
}
|
|
172
|
+
/** Cross-shell event bus. */
|
|
173
|
+
export interface EventsFacade {
|
|
174
|
+
/**
|
|
175
|
+
* Subscribe to named events from any of: the Kotlin bridge
|
|
176
|
+
* (`onSmartlinksData`), Capacitor plugin listeners, or internal pub/sub.
|
|
177
|
+
* Returns an unsubscribe function.
|
|
178
|
+
*/
|
|
179
|
+
subscribe(type: string, cb: (payload: unknown) => void): () => void;
|
|
180
|
+
/** Publish an event into the same bus. Useful for container ↔ container comms. */
|
|
181
|
+
emit(type: string, payload?: unknown): void;
|
|
182
|
+
}
|
|
183
|
+
/** OTA / web-source switching. */
|
|
184
|
+
export type WebSourceMode = 'live' | 'bundled' | 'channel';
|
|
185
|
+
export interface WebSourceConfig {
|
|
186
|
+
mode: WebSourceMode;
|
|
187
|
+
/** Only set when `mode === 'channel'`. */
|
|
188
|
+
channel?: string;
|
|
189
|
+
/** Only set when `mode === 'live'`. */
|
|
190
|
+
liveUrl?: string;
|
|
191
|
+
}
|
|
192
|
+
export interface WebSourceFacade {
|
|
193
|
+
/** Returns the currently active web source config. */
|
|
194
|
+
get(): Promise<WebSourceConfig>;
|
|
195
|
+
/** Switch web source mode. Takes effect on next app boot. */
|
|
196
|
+
set(config: WebSourceConfig): Promise<void>;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The full native capability facade exposed by the host on
|
|
200
|
+
* `AdminMobileHostContext.native` (containers) and `window.SL.native` (UMD
|
|
201
|
+
* microapps).
|
|
202
|
+
*
|
|
203
|
+
* Each sub-facade is present only when the host implements it. Check
|
|
204
|
+
* `'nfc' in host.native` (or `host.capabilities` for hardware-gated features)
|
|
205
|
+
* before calling.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* const { native } = host;
|
|
209
|
+
* if (native?.qr) {
|
|
210
|
+
* const code = await native.qr.scan();
|
|
211
|
+
* }
|
|
212
|
+
*/
|
|
213
|
+
export interface NativeFacade {
|
|
214
|
+
share: ShareFacade;
|
|
215
|
+
clipboard: ClipboardFacade;
|
|
216
|
+
haptics: HapticsFacade;
|
|
217
|
+
network: NetworkFacade;
|
|
218
|
+
device: DeviceFacade;
|
|
219
|
+
/** Cross-host persistent storage (`@capacitor/preferences` / `localStorage` fallback). */
|
|
220
|
+
storage: StorageFacade;
|
|
221
|
+
qr: QrFacade;
|
|
222
|
+
auth: AuthFacade;
|
|
223
|
+
nfc: NfcFacade;
|
|
224
|
+
/** RFID mass-scan. Only populated on `custom-android` hosts. */
|
|
225
|
+
rfid?: RfidFacade;
|
|
226
|
+
events: EventsFacade;
|
|
227
|
+
webSource: WebSourceFacade;
|
|
228
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract types for the `SL.native` / `host.native` facade.
|
|
3
|
+
*
|
|
4
|
+
* These are **interface-only** — no runtime implementation lives here.
|
|
5
|
+
* The facade is implemented per-host (Sidekick, Capacitor builds, PWA) and
|
|
6
|
+
* exposed to containers via `AdminMobileHostContext.native` or (for UMD
|
|
7
|
+
* microapps) via `window.SL.native`. The SDK owns the shape; the host owns
|
|
8
|
+
* the wiring.
|
|
9
|
+
*
|
|
10
|
+
* All facade methods reject with one of the structured errors from
|
|
11
|
+
* `@proveanything/smartlinks` (`HostCapabilityUnavailableError`,
|
|
12
|
+
* `HostPermissionDeniedError`, `HostTimeoutError`) so a single catch handler
|
|
13
|
+
* covers every host and capability.
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
package/docs/API_SUMMARY.md
CHANGED
package/docs/app-objects.md
CHANGED
|
@@ -85,6 +85,7 @@ Zones are **automatically filtered** based on the caller's role:
|
|
|
85
85
|
### Zone Writing Rules
|
|
86
86
|
|
|
87
87
|
- **Non-admin callers** attempting to write to the `admin` zone are silently ignored
|
|
88
|
+
- **Authenticated record owners** can write to `data` and `owner` by default; individual keys can be restricted via the `ownerEdit` app config policy (see [Owner Edit Policy](#owner-edit-policy) below)
|
|
88
89
|
- **Public callers** can write to `data` and `owner` (if visibility allows)
|
|
89
90
|
- **Admins** can write to all three zones
|
|
90
91
|
|
|
@@ -1098,6 +1099,61 @@ The `enforce` values are **merged over** the caller's request body, so you can l
|
|
|
1098
1099
|
|
|
1099
1100
|
---
|
|
1100
1101
|
|
|
1102
|
+
## Owner Edit Policy
|
|
1103
|
+
|
|
1104
|
+
Gives per-zone, field-level control over what an **authenticated record owner** can update via `PATCH /api/v1/public/collection/:collectionId/app/:appId/records/:recordId`.
|
|
1105
|
+
|
|
1106
|
+
Set the policy in the same app config document used for `publicCreate` (stored at `sites/{collectionId}/apps/{appId}`):
|
|
1107
|
+
|
|
1108
|
+
```json
|
|
1109
|
+
{
|
|
1110
|
+
"ownerEdit": {
|
|
1111
|
+
"records": {
|
|
1112
|
+
"data": { "allow": ["paypalEmail"] },
|
|
1113
|
+
"owner": { "allow": ["paypalEmail", "paypalEmailUpdatedAt"] }
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
### Zone visibility and write access
|
|
1120
|
+
|
|
1121
|
+
| Zone | Who can read | Who can write (owner) |
|
|
1122
|
+
|---------|------------------------|----------------------------------------------------------|
|
|
1123
|
+
| `data` | public | Allow-listed keys only (if policy set); all keys if not |
|
|
1124
|
+
| `owner` | owner + admin | Allow-listed keys only (if policy set); all keys if not |
|
|
1125
|
+
| `admin` | admin | Never — admin zone is always immutable to owners |
|
|
1126
|
+
|
|
1127
|
+
### Allow-list semantics
|
|
1128
|
+
|
|
1129
|
+
| Config | Behaviour |
|
|
1130
|
+
|----------------------------|-------------------------------------------------------------------------------|
|
|
1131
|
+
| No `ownerEdit` key | Default-allow — both zones fully writable (no change to existing behaviour) |
|
|
1132
|
+
| `allow` array with keys | Only the listed keys are accepted from the PATCH body; the rest are silently ignored and their existing values preserved |
|
|
1133
|
+
| `allow: []` (empty array) | Zone is effectively read-only for the owner |
|
|
1134
|
+
|
|
1135
|
+
Accepted keys are **merged** onto the existing zone blob — you do not need to re-send unchanged values.
|
|
1136
|
+
|
|
1137
|
+
### Example: commission record with protected fields
|
|
1138
|
+
|
|
1139
|
+
An app that lets owners update their payout email but not their commission total:
|
|
1140
|
+
|
|
1141
|
+
```json
|
|
1142
|
+
{
|
|
1143
|
+
"ownerEdit": {
|
|
1144
|
+
"records": {
|
|
1145
|
+
"owner": { "allow": ["paypalEmail", "paypalEmailUpdatedAt"] }
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
A PATCH body of `{ "owner": { "paypalEmail": "x@y.com", "totalCommission": 99 } }` will update `paypalEmail` only. `totalCommission` is silently ignored and its existing value is preserved.
|
|
1152
|
+
|
|
1153
|
+
> **App design note:** If your app creates records with sensitive fields that owners should never modify (e.g. computed totals, server-assigned fields), add an `ownerEdit` policy from the start. It is significantly easier to relax restrictions later than to tighten them after data has been mutated.
|
|
1154
|
+
|
|
1155
|
+
---
|
|
1156
|
+
|
|
1101
1157
|
## Anonymous Edit Tokens
|
|
1102
1158
|
|
|
1103
1159
|
Enables an anonymous caller to amend a record they just created — without authentication — by presenting a short-lived secret token.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
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
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.
|
|
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. For the full `host.native` facade contract (share, clipboard, haptics, NFC, RFID, storage, etc.) see [native-facade.md](native-facade.md).
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -23,6 +23,7 @@ This document describes how to build a **Mobile Admin Container** — a SmartLin
|
|
|
23
23
|
11. [Build & Bundle Requirements](#build--bundle-requirements)
|
|
24
24
|
12. [Example: Minimal Mobile Admin Container](#example-minimal-mobile-admin-container)
|
|
25
25
|
13. [Best Practices](#best-practices)
|
|
26
|
+
14. [Native Capability Facade](#native-capability-facade)
|
|
26
27
|
|
|
27
28
|
---
|
|
28
29
|
|
|
@@ -58,7 +59,7 @@ Your container **never** detects the host directly. It receives a `host` prop fr
|
|
|
58
59
|
|
|
59
60
|
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
|
|
|
61
|
-
> **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `ActionableCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `AdminMobileEventCallback`, `AdminMobileEventSubscriber`, `AdminMobileComponentManifest`, and `
|
|
62
|
+
> **SDK export** — `AdminMobileHostContext`, `AdminMobileCapability`, `ActionableCapability`, `AdminMobileHostId`, `AdminMobileEvent`, `AdminMobileEventCallback`, `AdminMobileEventSubscriber`, `AdminMobileComponentManifest`, `AdminMobileBundleManifest`, and `NativeFacade` (plus all sub-facade interfaces) are 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
63
|
|
|
63
64
|
```typescript
|
|
64
65
|
interface AdminMobileHostContext {
|
|
@@ -112,6 +113,10 @@ interface AdminMobileHostContext {
|
|
|
112
113
|
// Informational host version — use for logging/diagnostics, not feature detection.
|
|
113
114
|
// For feature detection prefer existence checks: 'requestNfcTap' in host.actions
|
|
114
115
|
_version: number
|
|
116
|
+
|
|
117
|
+
// Full native capability facade — share, clipboard, haptics, NFC, RFID, storage, etc.
|
|
118
|
+
// Optional: not every host stub populates it. See native-facade.md.
|
|
119
|
+
native?: NativeFacade
|
|
115
120
|
}
|
|
116
121
|
```
|
|
117
122
|
|
|
@@ -542,3 +547,26 @@ export const MOBILE_ADMIN_MANIFEST = {
|
|
|
542
547
|
- **Bundle Capacitor plugins in** (do not externalise) — so the component degrades gracefully on PWA/browser without crashing.
|
|
543
548
|
- **Declare `offline: true`** on a component if it queues writes locally — this signals the launcher to provision offline sync support.
|
|
544
549
|
- **Use `'methodName' in host.actions`** to feature-detect new host capabilities rather than comparing `host._version`. The version is informational only.
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Native Capability Facade
|
|
554
|
+
|
|
555
|
+
`host.native` gives containers access to a broader set of device capabilities — share sheet, clipboard, full haptics API, storage, QR, NFC, RFID (Kotlin only), auth, and cross-shell events — through a single interface that falls back gracefully across all host environments.
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
// Check host.hardware.* for physical availability, then call via host.native
|
|
559
|
+
if (host.hardware.nfc && host.native?.nfc) {
|
|
560
|
+
const { uid } = await host.native.nfc.read({ timeoutMs: 10_000 })
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Storage always available (Preferences → localStorage → in-memory Map)
|
|
564
|
+
await host.native?.storage.set('lastScan', uid)
|
|
565
|
+
|
|
566
|
+
// Share sheet with web fallback
|
|
567
|
+
await host.native?.share.share({ title: 'Found tag', url: `https://app.example/tags/${uid}` })
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
`host.native` is optional on `AdminMobileHostContext` — host stubs (Storybook, unit tests) need not implement it. `native.rfid` is additionally optional within `NativeFacade` itself, as it only exists on `custom-android`.
|
|
571
|
+
|
|
572
|
+
For the full sub-facade table, fallback chains, and what's intentionally not wrapped, see **[native-facade.md](native-facade.md)**.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Native Capability Facade (`host.native` / `SL.native`)
|
|
2
|
+
|
|
3
|
+
> **Version:** 1.12 · **Platform:** SmartLinks R4 · **Last updated:** 2026-04-30
|
|
4
|
+
|
|
5
|
+
The `NativeFacade` is a thin contract layer between microapps and the device capabilities available on the current host shell (Kotlin, Capacitor iOS/Android, PWA, or browser). It lets a microapp call `host.native.share.share({...})` without knowing whether it's running over `window.SmartlinksScanner`, a Capacitor plugin, or `navigator.share`.
|
|
6
|
+
|
|
7
|
+
**The SDK owns the contract** (`src/native/types.ts`, re-exported from `@proveanything/smartlinks`). Each host implementation is responsible for wiring up the sub-facades and populating:
|
|
8
|
+
- `AdminMobileHostContext.native` (container prop, typed in the SDK), and
|
|
9
|
+
- `window.SL.native` (for UMD microapps that don't receive a host prop — host concern, not SDK).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
1. [Access patterns](#access-patterns)
|
|
16
|
+
2. [What's in the facade](#whats-in-the-facade)
|
|
17
|
+
3. [What's NOT in the facade](#whats-not-in-the-facade)
|
|
18
|
+
4. [Error contract](#error-contract)
|
|
19
|
+
5. [Type reference](#type-reference)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Access patterns
|
|
24
|
+
|
|
25
|
+
### Inside a Mobile Admin Container (recommended)
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import type { AdminMobileHostContext } from '@proveanything/smartlinks'
|
|
29
|
+
|
|
30
|
+
function MyContainer({ host }: { host: AdminMobileHostContext }) {
|
|
31
|
+
const handleShare = async () => {
|
|
32
|
+
await host.native?.share.share({ title: 'SmartLinks', url: window.location.href })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleScan = async () => {
|
|
36
|
+
// Only present when the host provides a QR reader
|
|
37
|
+
const code = await host.native?.qr.scan()
|
|
38
|
+
if (code) processCode(code)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Always call via `host.native?.<facade>.<method>()` — `native` is optional because not every host exposes the full surface (e.g. a minimal browser stub may omit `rfid`).
|
|
44
|
+
|
|
45
|
+
For hardware-gated capabilities (NFC, RFID, QR, camera) also check `host.capabilities` or `host.hardware.*` before calling, as those flags reflect physical availability on the current device:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
if (host.hardware.nfc && host.native?.nfc) {
|
|
49
|
+
const { uid } = await host.native.nfc.read({ timeoutMs: 10_000 })
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Inside a UMD microapp (no host prop)
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// window.SL.native is populated by the host at boot — host concern, not SDK
|
|
57
|
+
const native = (window as any).SL?.native as import('@proveanything/smartlinks').NativeFacade | undefined
|
|
58
|
+
const code = await native?.qr.scan()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## What's in the facade
|
|
64
|
+
|
|
65
|
+
These capabilities have meaningful fallbacks across Kotlin / Capacitor / web. Always go through the facade.
|
|
66
|
+
|
|
67
|
+
| Sub-facade | Key methods | Fallback chain |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `native.share` | `share({ title, url, text? })`, `canShare()` | Capacitor Share → `navigator.share` → copy to clipboard + toast |
|
|
70
|
+
| `native.clipboard` | `write(text)`, `read()` | Capacitor Clipboard → `navigator.clipboard` → `execCommand('copy')` |
|
|
71
|
+
| `native.haptics` | `impact(style?)`, `notification(style)`, `selection()` | Capacitor Haptics → `navigator.vibrate` → no-op |
|
|
72
|
+
| `native.network` | `getStatus()`, `addListener('change', cb)` | Capacitor Network → `navigator.onLine` + `'online'`/`'offline'` events |
|
|
73
|
+
| `native.device` | `getInfo()`, `getId()`, `getLanguageCode()` | Capacitor Device → UA parsing + cached UUID in `localStorage` |
|
|
74
|
+
| `native.storage` | `get(key)`, `set(key, value)`, `remove(key)`, `keys()` | Capacitor Preferences → `localStorage` → in-memory `Map` (private mode) |
|
|
75
|
+
| `native.qr` | `scan(opts?)` | Capacitor MLKit Barcode → html5-qrcode / `BarcodeDetector` |
|
|
76
|
+
| `native.auth` | `signInWithGoogle()`, `signOut()` | Kotlin bridge → Capacitor Browser + OAuth → web redirect |
|
|
77
|
+
| `native.nfc` | `read(opts?)`, `writeNdef(opts)`, `programTag(opts)`, `lockTag(opts)`, `isLocked(opts)` | Kotlin bridge → Web NFC (`NDEFReader`) → `HostCapabilityUnavailableError` |
|
|
78
|
+
| `native.rfid` | `startScan(opts?)`, `stopScan()`, `subscribe(cb)` | Kotlin bridge only — throws `HostCapabilityUnavailableError` on all other hosts |
|
|
79
|
+
| `native.events` | `subscribe(type, cb)`, `emit(type, payload?)` | Internal pub/sub bridging Kotlin `onSmartlinksData` + Capacitor listeners |
|
|
80
|
+
| `native.webSource` | `get()`, `set({ mode, channel?, liveUrl? })` | Kotlin bridge + Capacitor Preferences mirror |
|
|
81
|
+
|
|
82
|
+
> **`native.rfid` is the only optional sub-facade** on `NativeFacade` — it is only populated on `custom-android` hosts. All other sub-facades are present on every host, though individual methods may throw `HostCapabilityUnavailableError` when the underlying capability is absent.
|
|
83
|
+
|
|
84
|
+
> **`native.device` vs `host.device`** — `host.device.info()` is a simplified convenience that returns `{ model, platform }`. `host.native?.device.getInfo()` returns the full `DeviceInfo` (adds `osVersion`, `manufacturer`, `isVirtual`) and also provides `getId()` and `getLanguageCode()`. Use `host.device` when the convenience is enough.
|
|
85
|
+
|
|
86
|
+
> **`native.network` vs `host.network`** — `host.network.isOnline()` is a boolean convenience. `host.native?.network.getStatus()` returns `{ connected, connectionType }` including `'wifi'` / `'cellular'` distinction.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## What's NOT in the facade
|
|
91
|
+
|
|
92
|
+
These capabilities intentionally have no `SL.native` wrapper. Call the Capacitor plugin directly, or use the web API. The host provides them as Tier 1 or Tier 2 plugins (see [Capacitor Plugin Baseline](mobile-admin-container.md#capacitor-plugin-baseline)).
|
|
93
|
+
|
|
94
|
+
| Capability | Why not wrapped |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `@capacitor/push-notifications` | Push only exists in Capacitor builds; no cross-shell registration flow. Import and call directly; detect Capacitor first. |
|
|
97
|
+
| `@capacitor/local-notifications` | Same — no Kotlin or web equivalent worth abstracting. |
|
|
98
|
+
| `@capacitor/camera` | Plugin already abstracts iOS/Android. PWA's `<input capture>` is too different in shape to unify. |
|
|
99
|
+
| `@capacitor/filesystem` | No web equivalent (storage quotas, sandbox rules differ). Use `URL.createObjectURL` + download anchor on web. |
|
|
100
|
+
| `@capacitor/geolocation` | `navigator.geolocation` already works in WebViews; the Capacitor plugin wraps the same OS APIs. No facade adds value. |
|
|
101
|
+
| `@capacitor/keyboard`, `@capacitor/screen-*`, `@capacitor/splash-screen`, `@capacitor/status-bar`, `@capacitor/dialog`, `@capacitor/app` | Per-host boot-time config or platform-chrome concerns; not part of the microapp contract. |
|
|
102
|
+
| `@capacitor/browser` | In-app browser for OAuth is intentionally Capacitor-only. For opening external URLs use `native.share.share({ url })`. |
|
|
103
|
+
| `@capawesome/capacitor-file-picker` | Picker UX too divergent across web (`<input type="file">`) and native to wrap usefully. |
|
|
104
|
+
|
|
105
|
+
If a capability later needs consistent behaviour across shells (e.g. notifications reach enough hosts to warrant throttling / routing via the facade), promote it into `NativeFacade` by adding an interface to `src/native/types.ts` and a row to the table above.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Error contract
|
|
110
|
+
|
|
111
|
+
All facade methods reject with one of the three structured errors from `@proveanything/smartlinks`. A single catch block handles every capability and host combination:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import {
|
|
115
|
+
HostCapabilityUnavailableError,
|
|
116
|
+
HostPermissionDeniedError,
|
|
117
|
+
HostTimeoutError,
|
|
118
|
+
} from '@proveanything/smartlinks'
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const { uid } = await host.native?.nfc.read({ timeoutMs: 10_000 }) ?? {}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err instanceof HostCapabilityUnavailableError) {
|
|
124
|
+
// host.capabilities won't list 'nfc' — we should have checked first
|
|
125
|
+
host.ui.toast?.({ title: 'NFC not available on this device', variant: 'destructive' })
|
|
126
|
+
} else if (err instanceof HostPermissionDeniedError) {
|
|
127
|
+
host.ui.toast?.({ title: 'NFC permission denied', variant: 'destructive' })
|
|
128
|
+
} else if (err instanceof HostTimeoutError) {
|
|
129
|
+
host.ui.toast?.({ title: `No tag detected after ${err.timeoutMs / 1000}s` })
|
|
130
|
+
} else {
|
|
131
|
+
throw err
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Type reference
|
|
139
|
+
|
|
140
|
+
All types below are exported from `@proveanything/smartlinks`:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import type {
|
|
144
|
+
NativeFacade, // root — use on AdminMobileHostContext.native
|
|
145
|
+
NativeCapability, // union of sub-facade keys: 'share' | 'clipboard' | ...
|
|
146
|
+
ShareFacade,
|
|
147
|
+
ClipboardFacade,
|
|
148
|
+
HapticsFacade,
|
|
149
|
+
HapticImpactStyle, // 'light' | 'medium' | 'heavy'
|
|
150
|
+
HapticNotificationStyle,
|
|
151
|
+
NetworkFacade,
|
|
152
|
+
NetworkStatus,
|
|
153
|
+
DeviceFacade,
|
|
154
|
+
DeviceInfo,
|
|
155
|
+
StorageFacade,
|
|
156
|
+
QrFacade,
|
|
157
|
+
QrScanOptions,
|
|
158
|
+
AuthFacade,
|
|
159
|
+
NfcFacade,
|
|
160
|
+
NfcReadResult,
|
|
161
|
+
RfidFacade,
|
|
162
|
+
RfidScanOptions,
|
|
163
|
+
EventsFacade,
|
|
164
|
+
WebSourceFacade,
|
|
165
|
+
WebSourceMode,
|
|
166
|
+
WebSourceConfig,
|
|
167
|
+
} from '@proveanything/smartlinks'
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The facade interfaces are contract-only — no runtime implementation is shipped in the SDK. You only need them for TypeScript type-checking (authoring containers or host stubs).
|