@condorinst/mobile-bridge 0.18.0 → 0.19.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/AGENTS.md +120 -0
- package/CHANGELOG.md +57 -0
- package/README.md +66 -11
- package/contract/bridge-contract.json +198 -0
- package/dist/cli.cjs +230 -17
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +231 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +64 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +442 -396
- package/dist/index.d.ts +442 -396
- package/dist/index.js +64 -64
- package/dist/index.js.map +1 -1
- package/docs/agent-guide.md +209 -0
- package/docs/bridge-contract.md +249 -0
- package/docs/client-guide.md +387 -0
- package/docs/limits.md +111 -0
- package/docs/pilot-onboarding.md +103 -0
- package/docs/troubleshooting.md +150 -0
- package/examples/minimal-home/AGENTS.md +28 -0
- package/examples/minimal-home/README.md +42 -0
- package/examples/minimal-home/build.mjs +31 -0
- package/examples/minimal-home/index.html +77 -0
- package/examples/minimal-home/package.json +23 -0
- package/examples/minimal-home/src/main.ts +161 -0
- package/examples/minimal-home/tsconfig.json +14 -0
- package/llms.txt +44 -0
- package/package.json +15 -2
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Build your White Label Home
|
|
2
|
+
|
|
3
|
+
This guide walks you — a Condor White Label partner — through building the web
|
|
4
|
+
Home that runs inside the Condor mobile app, using the
|
|
5
|
+
[`@condorinst/mobile-bridge`](../README.md) SDK.
|
|
6
|
+
|
|
7
|
+
No Flutter, Android, or iOS knowledge is required. You build a normal web app;
|
|
8
|
+
we package it and the app renders it.
|
|
9
|
+
|
|
10
|
+
> **This guide is the narrative path, written for a person reading start to
|
|
11
|
+
> finish.** If you are working with an AI agent, point it at
|
|
12
|
+
> [`AGENTS.md`](../AGENTS.md) and [`docs/agent-guide.md`](./agent-guide.md)
|
|
13
|
+
> instead — same platform, organised as a procedure with checks. The normative
|
|
14
|
+
> reference for keys, commands, errors and capabilities is
|
|
15
|
+
> [`docs/bridge-contract.md`](./bridge-contract.md); the hard numbers are in
|
|
16
|
+
> [`docs/limits.md`](./limits.md).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. What you're building
|
|
21
|
+
|
|
22
|
+
The app's Home is a **web bundle** that the Condor app renders inside a
|
|
23
|
+
sandboxed WebView (`<iframe sandbox="allow-scripts">`). You deliver a single
|
|
24
|
+
self-contained `index.html`; the app downloads it, verifies it, and shows it.
|
|
25
|
+
|
|
26
|
+
Your web code talks to the device and the native app through the **bridge** —
|
|
27
|
+
a typed message channel exposed by this SDK. You never touch Bluetooth or
|
|
28
|
+
native APIs directly: you subscribe to keys (device list, connection status,
|
|
29
|
+
real-time readings…) and send commands (refresh, start real-time, open
|
|
30
|
+
settings…).
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
┌────────────────────────────┐
|
|
34
|
+
│ Condor app (native) │
|
|
35
|
+
│ ┌──────────────────────┐ │
|
|
36
|
+
│ │ iframe (your bundle) │ │ ← your index.html runs here
|
|
37
|
+
│ │ BridgeClient ◄────────────► native bridge (devices, BLE, …)
|
|
38
|
+
│ └──────────────────────┘ │
|
|
39
|
+
└────────────────────────────┘
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Two hard rules** that shape everything below:
|
|
43
|
+
- The bundle must be **one `index.html`**, fully **self-contained** (JS/CSS
|
|
44
|
+
inline, media as `data:` URIs). Relative paths and external URLs do **not**
|
|
45
|
+
load inside the sandbox.
|
|
46
|
+
- The bundle must be **≤ 2 MB** (zipped).
|
|
47
|
+
|
|
48
|
+
The CLI in this SDK turns your normal build into a compliant bundle for you
|
|
49
|
+
(see §6) — you don't have to inline by hand.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. Install the SDK
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install @condorinst/mobile-bridge
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Pin the version with a caret:
|
|
60
|
+
|
|
61
|
+
```jsonc
|
|
62
|
+
// package.json
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@condorinst/mobile-bridge": "^0.19.0"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Updates.** The SDK is on `0.x` during the pilot. With `^0.19.0`, running
|
|
69
|
+
`npm install` (or your CI) auto-picks **patch** releases (`0.8.x`) — bug fixes,
|
|
70
|
+
no action needed. A `0.9.0` (a larger change during beta) is an intentional
|
|
71
|
+
opt-in: you bump the range when you're ready. `1.0.0` will mark the
|
|
72
|
+
production-stable contract.
|
|
73
|
+
|
|
74
|
+
The published package ships prebuilt JS + TypeScript types — no build tooling
|
|
75
|
+
of ours required on your side.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 3. Talk to the app — the bridge
|
|
80
|
+
|
|
81
|
+
Import **`BridgeClient`** (the model for code running inside the app's iframe):
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Handshake
|
|
88
|
+
|
|
89
|
+
Announce yourself once at boot. The ack tells you the bridge is live.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
|
|
93
|
+
// ack.accepted: boolean
|
|
94
|
+
// ack.shellVersion: string
|
|
95
|
+
// ack.nativeVersion: string | null — the native's schema version, relayed by
|
|
96
|
+
// the shell (0.17.0+). `null` means the native has not answered or is absent
|
|
97
|
+
// (browser mock). There is no version negotiation, so this is the only signal
|
|
98
|
+
// telling you whether a capability exists — e.g. the raw `light_sensor_*`
|
|
99
|
+
// channels on `deviceLightSensors` require native >= 0.16.0.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Listen for events (native → you)
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const unsubscribe = BridgeClient.on(BRIDGE_KEYS.deviceConnection, (payload) => {
|
|
106
|
+
// payload is typed: { serialNo: number, isConnect: boolean }
|
|
107
|
+
console.log(payload.serialNo, payload.isConnect);
|
|
108
|
+
});
|
|
109
|
+
// later: unsubscribe();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Send commands (you → native)
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
BridgeClient.send('userDevice', { getFromServer: true }); // refresh device list
|
|
116
|
+
BridgeClient.send('realTime', { serialNo: 1234, value: true }); // start real-time
|
|
117
|
+
BridgeClient.openSettings(); // open native Settings
|
|
118
|
+
BridgeClient.requestPermission('bluetoothScan'); // in-app permission dialog
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
> Commands are gated by the app's policy. A command that isn't allowed is
|
|
122
|
+
> silently ignored — design your UI to not depend on a response you won't get.
|
|
123
|
+
|
|
124
|
+
### Handle the system back button
|
|
125
|
+
|
|
126
|
+
Your page runs inside the app's Home screen. On Android, the OS back
|
|
127
|
+
button/gesture would otherwise leave the Home — so the app delegates it to you
|
|
128
|
+
when you opt in, letting the back button walk **your** internal navigation
|
|
129
|
+
(e.g. a device detail page → back to the list) before it does anything else.
|
|
130
|
+
|
|
131
|
+
Two calls wire it up:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// 1. Tell the app whether you have an internal page to go back to.
|
|
135
|
+
// Call this on EVERY route change (and once at boot).
|
|
136
|
+
BridgeClient.reportNavigation(currentView !== 'home'); // true = you can go back
|
|
137
|
+
|
|
138
|
+
// 2. React to the OS back button. Called only when you last reported `true`.
|
|
139
|
+
BridgeClient.onBack(() => {
|
|
140
|
+
goToPreviousPage(); // pop one internal page, then reportNavigation(...) again
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
How it behaves:
|
|
145
|
+
|
|
146
|
+
- You reported `true` → the OS back button fires your `onBack` (go back one
|
|
147
|
+
internal page). Nothing else happens — the app stays on Home.
|
|
148
|
+
- You reported `false` (or never adopted this API) → the OS back button is
|
|
149
|
+
ignored and the user stays on Home. **No regression if you skip it.**
|
|
150
|
+
|
|
151
|
+
Keep `reportNavigation` in sync with your router: the app caches the last value
|
|
152
|
+
and reads it the instant back is pressed. A single choke point (call it at the
|
|
153
|
+
end of your `render`/route function) is the simplest way to never miss a change.
|
|
154
|
+
See `examples/client_demo/src/main.ts` for a worked home↔device example.
|
|
155
|
+
|
|
156
|
+
### Key catalog
|
|
157
|
+
|
|
158
|
+
These are the keys you can subscribe to / send. Payloads are fully typed in
|
|
159
|
+
TypeScript; the table is the quick reference (see the
|
|
160
|
+
[README](../README.md#key-catalog-strictly-typed) for exact shapes).
|
|
161
|
+
|
|
162
|
+
| Key | Payload (typed) | Direction |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `userDevice` | device config / telemetry / user config arrays | native → you (also send to refresh) |
|
|
165
|
+
| `deviceConnection` | `{ serialNo, isConnect }` | native → you (status) |
|
|
166
|
+
| `deviceNotification` | `{ serialNo, isConnect, message, messageCode, isError }` | native → you |
|
|
167
|
+
| `deviceLightSensors` | `{ serialNo, type, data }` — union on `type` (`'actlumus'` \| `'actlumus2'`); `data` has the spectrum plus the raw `light_sensor_*` channels | native → you |
|
|
168
|
+
| `realTime` | `{ serialNo, success, value }` | both (command + state) |
|
|
169
|
+
| `syncTime` | `{ serialNo, success }` | both |
|
|
170
|
+
| `sensorLog` | paginated sensor log envelope | native → you |
|
|
171
|
+
| `systemStatus` | `{ bluetooth, internet, permission, hasDevices, missingOptionalPermissions, missingEssentialPermissions, deviceSyncError }` | native → you (env status) |
|
|
172
|
+
| `deleteMemory` | `{ serialNo, success }` | both |
|
|
173
|
+
| `pingPong` | `{ data: 'ping' \| 'pong' }` | both |
|
|
174
|
+
| `navigationBack` | `{}` (OS back pressed — via `onBack`) | native → you |
|
|
175
|
+
| `language` | `{ deviceLocale, appLanguage, resolvedLanguage }` | native → you (language state) |
|
|
176
|
+
|
|
177
|
+
> **System back button.** Use the `reportNavigation(canGoBack)` /
|
|
178
|
+
> `onBack(cb)` helpers (see [Handle the system back button](#handle-the-system-back-button))
|
|
179
|
+
> rather than subscribing to `navigationBack` directly — `reportNavigation`
|
|
180
|
+
> sends the `navigationState` command the app needs to route the back button.
|
|
181
|
+
|
|
182
|
+
**Permissions on `systemStatus`.** Two arrays of `AppPermission` names let you
|
|
183
|
+
render permission prompts from your own UI:
|
|
184
|
+
|
|
185
|
+
- `missingEssentialPermissions` — **required** for the app to work; background
|
|
186
|
+
collection stays off until granted. Render a **blocking "Required permissions"
|
|
187
|
+
card** (e.g. `["bluetoothScan", "bluetoothConnect"]`).
|
|
188
|
+
- `missingOptionalPermissions` — **recommended** (help background reliability);
|
|
189
|
+
render a **dismissible "Recommended permissions" nudge** (e.g.
|
|
190
|
+
`["notification", "backgroundLocation"]`).
|
|
191
|
+
|
|
192
|
+
Both are empty when nothing is missing. For the **required** card, wire the
|
|
193
|
+
"Grant" button to `BridgeClient.requestPermission(name)` — it fires the **native
|
|
194
|
+
in-app permission dialog** for that `AppPermission` (falls back to Settings only
|
|
195
|
+
when the OS refuses the prompt). Pass a name straight from
|
|
196
|
+
`missingEssentialPermissions` (e.g. `'bluetoothScan'`). There is no dedicated
|
|
197
|
+
result key: the native side re-emits `systemStatus` once granted, so the card
|
|
198
|
+
disappears when `missingEssentialPermissions` empties. `openSettings` remains
|
|
199
|
+
available for the optional nudge (permissions not grantable by prompt on modern
|
|
200
|
+
Android). See `examples/client_demo/src/render-home.ts` for a worked render.
|
|
201
|
+
|
|
202
|
+
### Translate your bundle — the `language` key
|
|
203
|
+
|
|
204
|
+
The app tells you **two** languages, and the distinction between them is the
|
|
205
|
+
whole point of the channel:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
BridgeClient.on(BRIDGE_KEYS.language, ({ deviceLocale, appLanguage, resolvedLanguage }) => {
|
|
209
|
+
// Your precedence rule. This one honours an explicit choice, else the device.
|
|
210
|
+
const lang = appLanguage ?? pickSupported(deviceLocale) ?? 'en';
|
|
211
|
+
renderIn(lang);
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- **`deviceLocale`** — the raw device locale as a BCP-47 tag (`"pt-BR"`,
|
|
216
|
+
`"es-419"`, `"ja-JP"`), exactly as the OS reports it, region included.
|
|
217
|
+
- **`appLanguage`** — the language the user explicitly picked in the app
|
|
218
|
+
Settings (`"en" | "pt" | "de" | "fr"`), or **`null`** when they never picked
|
|
219
|
+
one.
|
|
220
|
+
- **`resolvedLanguage`** — the language the native chrome (splash, Settings,
|
|
221
|
+
loading/error screens) is rendering right now.
|
|
222
|
+
|
|
223
|
+
**Why two raw values instead of one answer.** The app itself ships four
|
|
224
|
+
languages and collapses everything else to English. Your bundle has no such
|
|
225
|
+
limit — it may ship Spanish, Italian, Japanese. If we sent you only the resolved
|
|
226
|
+
language, a device set to Spanish would reach you as `en` and you would render
|
|
227
|
+
English while holding a Spanish bundle. The information would have been
|
|
228
|
+
destroyed on our side before reaching you. So we send both and let you decide:
|
|
229
|
+
|
|
230
|
+
| `deviceLocale` | `appLanguage` | a reasonable decision |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| `es-ES` | `null` | render **Spanish** — you have it, we don't. Our chrome stays English while your content is Spanish, which is better, not worse |
|
|
233
|
+
| `es-ES` | `"pt"` | render **Portuguese** — an explicit choice outranks the device |
|
|
234
|
+
| `pt-BR` | `null` | render Portuguese |
|
|
235
|
+
| `ja-JP` | `"de"` | render German (the explicit choice), or Japanese if you'd rather honour the device — your call, with both values in hand |
|
|
236
|
+
|
|
237
|
+
`appLanguage` is `null` when — and only when — the user never chose. It is never
|
|
238
|
+
a fallback in disguise, so `null` is a real signal you can branch on. Use
|
|
239
|
+
`resolvedLanguage` when you'd rather stay visually consistent with the native
|
|
240
|
+
frame around your page than follow the device.
|
|
241
|
+
|
|
242
|
+
**When it arrives — and how to ask for it.** The app pushes this key on attach,
|
|
243
|
+
again right after the handshake ack, and on every change in the app Settings, so
|
|
244
|
+
a page already loaded re-renders without a restart. But the first push happens
|
|
245
|
+
while the WebView is still being created, before your bundle exists, so **send
|
|
246
|
+
the `language` command once after your handshake** to pull the current state:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
BridgeClient.send('language', {}); // replies with a `language` event
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Without that pull, a first launch would show your default language until the
|
|
253
|
+
user changed it in Settings. Subscribe first, then pull.
|
|
254
|
+
|
|
255
|
+
Do not read `navigator.language` instead: it follows the system, not the in-app
|
|
256
|
+
choice, and you would end up in Spanish while the native chrome around you is in
|
|
257
|
+
Portuguese.
|
|
258
|
+
|
|
259
|
+
**Older app versions.** There is no version negotiation. An app older than
|
|
260
|
+
native `0.14.0` never emits this key, so the event simply never arrives — keep a
|
|
261
|
+
default language for that case, and read `nativeVersion` from the handshake ack
|
|
262
|
+
if you need to branch on it explicitly.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 4. Develop in your browser (no app needed)
|
|
267
|
+
|
|
268
|
+
When your code runs in a normal browser (not inside the app), the SDK
|
|
269
|
+
auto-activates **mock mode**: the bridge no longer expects a native side, and
|
|
270
|
+
you get tooling to simulate it.
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
274
|
+
|
|
275
|
+
if (Bridge.isMock) {
|
|
276
|
+
// Floating dev panel: inspect sent messages and emit native events by hand.
|
|
277
|
+
Bridge.installDevPanel({ position: 'bottom-right', theme: 'dark' });
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The dev panel lets you:
|
|
282
|
+
- See every command your UI sends.
|
|
283
|
+
- Emit any `BRIDGE_KEYS` event with a JSON payload (simulate the device).
|
|
284
|
+
- Enable **auto-respond** so a `send` triggers a canned `emit` — closing the
|
|
285
|
+
full loop without manual clicks (provide an `onSendForAutoRespond` policy;
|
|
286
|
+
see the [README](../README.md#auto-respond-toggle)).
|
|
287
|
+
|
|
288
|
+
> In mock mode the handshake does **not** resolve on its own — emit a
|
|
289
|
+
> `handshake-ack` (the dev panel and auto-respond can do this for you). This is
|
|
290
|
+
> intentional, so your tests are explicit about the native state.
|
|
291
|
+
|
|
292
|
+
The [`examples/minimal-home`](../examples/minimal-home) example is set up to run
|
|
293
|
+
this way out of the box.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## 5. Bundle format & constraints
|
|
298
|
+
|
|
299
|
+
Your final artifact is a `.zip` containing exactly **one self-contained
|
|
300
|
+
`index.html`** at the root. Why the constraints:
|
|
301
|
+
|
|
302
|
+
- The app injects your HTML via `iframe.srcdoc` with origin `null` and no base
|
|
303
|
+
URL → **relative references don't resolve** (`<script src="app.js">`,
|
|
304
|
+
`<img src="logo.png">` would break).
|
|
305
|
+
- The app's Content-Security-Policy **blocks external fetches** → CDN scripts,
|
|
306
|
+
Google Fonts, remote APIs called at load time won't work.
|
|
307
|
+
|
|
308
|
+
So the bundle must have:
|
|
309
|
+
- ✅ One `index.html` at the zip root.
|
|
310
|
+
- ✅ JS and CSS **inlined** into the HTML.
|
|
311
|
+
- ✅ Images/icons/fonts as **`data:` URIs**.
|
|
312
|
+
- ✅ Total size **≤ 2 MB** (zipped). Note base64 inflates media ~33%.
|
|
313
|
+
- ❌ No external `https://` script/style/font references loaded at runtime.
|
|
314
|
+
- ❌ No code-splitting / dynamic `import()` across chunks (see below).
|
|
315
|
+
|
|
316
|
+
### Single-chunk build
|
|
317
|
+
|
|
318
|
+
The bundler CLI is post-build and assumes **one JS + one CSS + assets**. If
|
|
319
|
+
your framework code-splits, the split chunks won't resolve in the sandbox.
|
|
320
|
+
Configure a single-file build:
|
|
321
|
+
|
|
322
|
+
- **Vite:** add [`vite-plugin-singlefile`](https://github.com/richardtallent/vite-plugin-singlefile),
|
|
323
|
+
or set `build.rollupOptions.output.inlineDynamicImports: true` +
|
|
324
|
+
`manualChunks: undefined`.
|
|
325
|
+
- **Create React App:** avoid `React.lazy` / dynamic `import()` in routes so the
|
|
326
|
+
build emits a single bundle.
|
|
327
|
+
- **esbuild:** bundle to a single IIFE/ESM file (see the example's `build.mjs`).
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 6. Build the bundle
|
|
332
|
+
|
|
333
|
+
Once your build produces a `dist/index.html` (referencing a local `main.js`,
|
|
334
|
+
CSS, images), run the CLI to inline everything and pack the zip:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
It inlines `<script src>`, `<link rel=stylesheet>`, CSS `url()`, and local
|
|
341
|
+
`<img>`/icons into a single `index.html`, then zips it. Output:
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
Built dist/bundle.zip
|
|
345
|
+
entry: index.html (123.4 KB self-contained)
|
|
346
|
+
bundleSize (zip): 45678 bytes
|
|
347
|
+
bundleHash (zip): sha256-<hex>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
| Option | Description |
|
|
351
|
+
|---|---|
|
|
352
|
+
| `-i, --html <path>` | Input HTML. Required. |
|
|
353
|
+
| `-o, --out <path>` | Output `.zip` (default `dist/whitelabel-bundle.zip`). |
|
|
354
|
+
| `--max-bytes <n>` | Warning threshold (default 2 MB). |
|
|
355
|
+
|
|
356
|
+
The hash is deterministic — the same input always yields the same
|
|
357
|
+
`bundleHash`. The CLI **warns** above 2 MB but the **app rejects** oversized
|
|
358
|
+
bundles and falls back to a default screen, so keep under the limit.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 7. Deliver
|
|
363
|
+
|
|
364
|
+
Send the resulting `dist/bundle.zip` through the publication channel Condor
|
|
365
|
+
gave you (the admin upload). The backend validates structure, self-containment,
|
|
366
|
+
and size on upload, and records the `bundleSize`/`bundleHash`. Once published,
|
|
367
|
+
the app downloads, verifies, and renders your Home.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 8. Troubleshooting
|
|
372
|
+
|
|
373
|
+
| Symptom | Likely cause |
|
|
374
|
+
|---|---|
|
|
375
|
+
| Blank screen / nothing loads | Relative `<script src>` or external URL — must be inlined. Run the build CLI. |
|
|
376
|
+
| Images missing | Referenced by path instead of `data:` URI — the CLI inlines local images; remote images are blocked. |
|
|
377
|
+
| Works in browser, not in app | You relied on a network call or external asset at load time (CSP blocks it), or a code-split chunk didn't resolve. Go single-chunk + self-contained. |
|
|
378
|
+
| Bundle rejected on upload / falls back | Over 2 MB, multiple files at root, or not self-contained. |
|
|
379
|
+
| `handshake` never resolves in browser | Expected in mock mode — emit a `handshake-ack` (dev panel / auto-respond). |
|
|
380
|
+
| A command does nothing | The command may not be allowed by the app policy; it's dropped silently. |
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Next steps
|
|
385
|
+
|
|
386
|
+
- Copy [`examples/minimal-home`](../examples/minimal-home) and start from there.
|
|
387
|
+
- Full API reference: the [README](../README.md).
|
package/docs/limits.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Limits
|
|
2
|
+
|
|
3
|
+
Every number here is enforced in code. The **On violation** column is what you
|
|
4
|
+
actually observe — which matters more than the number, because most of these
|
|
5
|
+
fail without telling you.
|
|
6
|
+
|
|
7
|
+
Each row cites the file and line in the Canvas app that applies it, as verified
|
|
8
|
+
against monorepo commit `a6b19cd2` on 2026-09-22. The same values are available
|
|
9
|
+
as JSON in `contract/bridge-contract.json`, including these citations under
|
|
10
|
+
`verifiedAgainst.sources`.
|
|
11
|
+
|
|
12
|
+
## Messages
|
|
13
|
+
|
|
14
|
+
| Limit | Value | On violation | Enforced at |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| Payload, JS → native | 64 KB (65536 bytes) | **Silent drop.** Nothing reaches the native side and nothing comes back. | `condor_bridge.dart:88,229-238` |
|
|
17
|
+
| `jsError` rate | 20 per rolling 10 s | Silent drop of the excess. | `condor_bridge.dart:93-94,311-317` |
|
|
18
|
+
| `jsError` field length | 4 KB each for `message` and `stack` | Truncated, with a marker appended. | `condor_bridge.dart:97,319-320` |
|
|
19
|
+
|
|
20
|
+
64 KB is generous for commands but not for data you might try to push back.
|
|
21
|
+
There is no chunking protocol: if you need to move something larger, you are
|
|
22
|
+
solving the wrong problem — the native side is the one that holds data.
|
|
23
|
+
|
|
24
|
+
## Timeouts
|
|
25
|
+
|
|
26
|
+
| Operation | Timeout | On expiry |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `BridgeClient.handshake` | 5 s | Promise **rejects**. |
|
|
29
|
+
| `BridgeClient.fetchSensorLog` | 10 s (override per call) | Promise **rejects**. This is the only rejection path — a failed query *resolves* with `success: false`. |
|
|
30
|
+
| `BridgeShell.verifySandbox` | 2 s | Resolves with the failure recorded. |
|
|
31
|
+
| Relay of `nativeVersion` into the iframe | 2 s | `ack.nativeVersion` comes back `null`. |
|
|
32
|
+
|
|
33
|
+
## Sensor log
|
|
34
|
+
|
|
35
|
+
| Parameter | Range | On violation |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `limit` | 1–1000 (backend default 10) | `success: false`, `error: "INVALID_LIMIT: <value>"` |
|
|
38
|
+
| `bodyPart` | `wrist` or `necklace` | `error: "INVALID_BODY_PART: <value>"` |
|
|
39
|
+
| `startingAfter` + `endingBefore` | mutually exclusive | `error: "CONFLICTING_CURSORS"` |
|
|
40
|
+
| `startTime`, `endTime` | timestamp format, native ≥ 0.15.0 | `error: "INVALID_START_TIME"` / `"INVALID_END_TIME"` |
|
|
41
|
+
|
|
42
|
+
Cursors are **opaque**. Echo back exactly what you received; never construct or
|
|
43
|
+
parse one.
|
|
44
|
+
|
|
45
|
+
Verified at `sensor_log_entity.dart:70-85`.
|
|
46
|
+
|
|
47
|
+
## Bundle
|
|
48
|
+
|
|
49
|
+
| Limit | Value | On violation |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `.zip` size | 2 MB (2097152 bytes) | **The app rejects the bundle**, keeps the previous cached version, or falls back to its default screen. |
|
|
52
|
+
| Entry document | one `index.html` at the archive root | Falls back to the first `.html` found; if there is none, the bundle is rejected. |
|
|
53
|
+
| External `<script src>` | forbidden | Bundle rejected. |
|
|
54
|
+
| External `<link rel="stylesheet">` | forbidden | Bundle rejected. |
|
|
55
|
+
| Presigned download URL | valid 24 h | The app re-fetches the manifest. |
|
|
56
|
+
|
|
57
|
+
Verified at `whitelabel_bundle_limits.dart:14` and
|
|
58
|
+
`whitelabel_bundle_service.dart:227-398`.
|
|
59
|
+
|
|
60
|
+
`data:` URIs inflate binary assets by roughly a third over their raw bytes, so
|
|
61
|
+
budget against the inflated size. `npx condor-mobile-bridge build` prints the
|
|
62
|
+
final `.zip` size; `validate` fails on it.
|
|
63
|
+
|
|
64
|
+
## Storage: there is none
|
|
65
|
+
|
|
66
|
+
| API | Behaviour |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `localStorage`, `sessionStorage` | throw `SecurityError` |
|
|
69
|
+
| `indexedDB` | unavailable |
|
|
70
|
+
| `document.cookie` | writes are silently ignored |
|
|
71
|
+
| Service workers | cannot be registered |
|
|
72
|
+
| Cache API | unavailable |
|
|
73
|
+
|
|
74
|
+
The shell is loaded with no base URL, giving it an opaque origin; your iframe is
|
|
75
|
+
`sandbox="allow-scripts"` rendered from `srcdoc`, giving it a `null` origin.
|
|
76
|
+
Both are deliberate — the opaque origin is what lets the app guarantee that
|
|
77
|
+
logging out leaves nothing behind.
|
|
78
|
+
|
|
79
|
+
Hold state in memory. It is lost when the WebView reloads, which happens when
|
|
80
|
+
the bundle is updated, when the kill switch fires, and when the person changes
|
|
81
|
+
the app language while no client bundle is active.
|
|
82
|
+
|
|
83
|
+
Verified at `home_page.dart:296-298`.
|
|
84
|
+
|
|
85
|
+
## Network: there is none
|
|
86
|
+
|
|
87
|
+
The shell CSP is:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';
|
|
91
|
+
img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-src 'self';
|
|
92
|
+
object-src 'none'; base-uri 'none'; form-action 'none';
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
It is inherited by your `srcdoc` iframe and **an iframe cannot relax the policy
|
|
96
|
+
it inherits**. With a `null` origin, `'self'` matches nothing, so `connect-src
|
|
97
|
+
'self'` blocks every outbound request. No analytics, no CDN, no error reporting
|
|
98
|
+
service.
|
|
99
|
+
|
|
100
|
+
For error reporting, use the `jsError` command — it reaches Condor's logging
|
|
101
|
+
and Crashlytics, within the rate limit above.
|
|
102
|
+
|
|
103
|
+
Verified at `shell.template.html:28`.
|
|
104
|
+
|
|
105
|
+
## Devices
|
|
106
|
+
|
|
107
|
+
There is **no device cap in the Canvas app**. A search for `maxDevices` or
|
|
108
|
+
`deviceLimit` across the app returns nothing. If your product has a limit, it
|
|
109
|
+
comes from the backend or from commercial terms — not from this platform. Do
|
|
110
|
+
not hard-code one, and do not assume the roster you receive is complete or
|
|
111
|
+
bounded.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# White Label — Pilot Onboarding
|
|
2
|
+
|
|
3
|
+
Welcome! This is the fast path to building and shipping your **White Label
|
|
4
|
+
Home** — the screen that runs inside the Condor app. You build a normal web app;
|
|
5
|
+
we package and render it. No Flutter / Android / iOS needed.
|
|
6
|
+
|
|
7
|
+
For the full reference, see the [Client Guide](./client-guide.md). This page is
|
|
8
|
+
the quick checklist to get a pilot through the whole loop.
|
|
9
|
+
|
|
10
|
+
## What you'll end up with
|
|
11
|
+
|
|
12
|
+
A self-contained `bundle.zip` that Condor publishes to your tenant — your Home
|
|
13
|
+
renders inside the app, talking to the device through the Condor bridge.
|
|
14
|
+
|
|
15
|
+
## Who does what
|
|
16
|
+
|
|
17
|
+
| Condor provides | You build |
|
|
18
|
+
|---|---|
|
|
19
|
+
| The SDK (`@condorinst/mobile-bridge`), guide & example | Your Home web app (HTML/CSS/JS or React) |
|
|
20
|
+
| The app that renders your bundle + the device bridge | The `bundle.zip` (via our CLI) |
|
|
21
|
+
| The publish channel (admin upload) | Delivering the `.zip` to us |
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- Node.js 18+
|
|
26
|
+
- npm access (the package is public — no credentials needed)
|
|
27
|
+
|
|
28
|
+
## Step-by-step
|
|
29
|
+
|
|
30
|
+
### 1. Install the SDK
|
|
31
|
+
```bash
|
|
32
|
+
npm install @condorinst/mobile-bridge
|
|
33
|
+
```
|
|
34
|
+
Pin `"@condorinst/mobile-bridge": "^0.8.0"` (beta; auto-updates patches).
|
|
35
|
+
|
|
36
|
+
### 2. Start from the example
|
|
37
|
+
Copy [`examples/minimal-home`](../examples/minimal-home) — it already wires the
|
|
38
|
+
bridge, renders devices, and builds a bundle.
|
|
39
|
+
|
|
40
|
+
### 3. Build your Home
|
|
41
|
+
Talk to the app via `BridgeClient`:
|
|
42
|
+
```ts
|
|
43
|
+
import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
44
|
+
|
|
45
|
+
await BridgeClient.handshake({ clientVersion: '1.0.0' }); // announce yourself
|
|
46
|
+
BridgeClient.on(BRIDGE_KEYS.deviceConnection, (p) => { /* ... */ }); // listen
|
|
47
|
+
BridgeClient.send('userDevice', { getFromServer: true }); // command
|
|
48
|
+
BridgeClient.openSettings(); // open native settings
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 4. Test in your browser (no app needed)
|
|
52
|
+
Outside the app the SDK auto-enables **mock mode** + a dev panel (bottom-right)
|
|
53
|
+
to simulate device events and close the loop.
|
|
54
|
+
|
|
55
|
+
### 5. Build the bundle
|
|
56
|
+
```bash
|
|
57
|
+
npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
|
|
58
|
+
```
|
|
59
|
+
This inlines everything into a single self-contained `index.html` and zips it.
|
|
60
|
+
It prints `bundleSize` + `bundleHash`.
|
|
61
|
+
|
|
62
|
+
### 5b. Validate before you deliver
|
|
63
|
+
```bash
|
|
64
|
+
npx condor-mobile-bridge validate -i dist/bundle.zip --strict
|
|
65
|
+
```
|
|
66
|
+
Applies the same gates the app applies on download, plus a lint for the APIs the
|
|
67
|
+
sandbox and the CSP block at runtime. **A non-zero exit means do not deliver.**
|
|
68
|
+
Errors are outright rejections by the app; warnings install fine and then
|
|
69
|
+
misbehave on the device, which is harder to diagnose.
|
|
70
|
+
|
|
71
|
+
### 6. Deliver
|
|
72
|
+
Send `dist/bundle.zip` through the channel Condor gave you. We publish it to your
|
|
73
|
+
tenant.
|
|
74
|
+
|
|
75
|
+
### 7. Verify on a device
|
|
76
|
+
Condor publishes → the app downloads, validates and renders your Home.
|
|
77
|
+
|
|
78
|
+
## Hard rules (or the app rejects the bundle)
|
|
79
|
+
|
|
80
|
+
- ✅ Exactly **one `index.html`** at the zip root
|
|
81
|
+
- ✅ JS/CSS **inline**, media as **`data:` URIs**, no external/relative refs loaded at runtime
|
|
82
|
+
- ✅ **≤ 2 MB** (zipped)
|
|
83
|
+
- ✅ **Single-chunk build** (no code-splitting): Vite → `vite-plugin-singlefile`; CRA → no `React.lazy` / dynamic `import()`
|
|
84
|
+
|
|
85
|
+
The CLI handles the inlining/zipping for you — just keep your build single-file.
|
|
86
|
+
|
|
87
|
+
## Reference
|
|
88
|
+
|
|
89
|
+
- [Client Guide](./client-guide.md) — full walkthrough (bridge API, key catalog, mock mode, dev panel)
|
|
90
|
+
- [`examples/minimal-home`](../examples/minimal-home) — runnable minimal client
|
|
91
|
+
- [README](../README.md) — API reference
|
|
92
|
+
|
|
93
|
+
## Pilot checklist
|
|
94
|
+
|
|
95
|
+
- [ ] SDK installed (`npm i @condorinst/mobile-bridge`)
|
|
96
|
+
- [ ] Example runs in the browser (mock mode)
|
|
97
|
+
- [ ] Home built with `BridgeClient` (handshake + at least one key + one command)
|
|
98
|
+
- [ ] Constraints respected (1 index.html, ≤2 MB, self-contained, single-chunk)
|
|
99
|
+
- [ ] `bundle.zip` generated by the CLI
|
|
100
|
+
- [ ] `.zip` delivered to Condor
|
|
101
|
+
- [ ] Home verified on a device
|
|
102
|
+
- [ ] Feedback shared with Condor
|
|
103
|
+
</content>
|