@condorinst/mobile-bridge 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,400 +1,34 @@
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.22.0"
65
- }
66
- ```
67
-
68
- **Updates.** The SDK is on `0.x` during the pilot. With `^0.22.0`, running
69
- `npm install` (or your CI) auto-picks **patch** releases (`0.22.x`) — bug fixes,
70
- no action needed. A `0.23.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
- const devices = await BridgeClient.requestUserDevices(); // device list, resent until it arrives
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
- > Ask for the device list with `requestUserDevices()`, not with
125
- > `send('userDevice')`: the first request after the app starts is often lost,
126
- > and an account with no device gets no reply at all. The helper resends with
127
- > backoff and resolves `{ status: 'timeout', hasDevices }` after 30 s, so you can
128
- > show an empty state. See [Troubleshooting](./troubleshooting.md).
129
-
130
- ### Handle the system back button
131
-
132
- Your page runs inside the app's Home screen. On Android, the OS back
133
- button/gesture would otherwise leave the Home — so the app delegates it to you
134
- when you opt in, letting the back button walk **your** internal navigation
135
- (e.g. a device detail page → back to the list) before it does anything else.
136
-
137
- Two calls wire it up:
138
-
139
- ```typescript
140
- // 1. Tell the app whether you have an internal page to go back to.
141
- // Call this on EVERY route change (and once at boot).
142
- BridgeClient.reportNavigation(currentView !== 'home'); // true = you can go back
143
-
144
- // 2. React to the OS back button. Called only when you last reported `true`.
145
- BridgeClient.onBack(() => {
146
- goToPreviousPage(); // pop one internal page, then reportNavigation(...) again
147
- });
148
- ```
149
-
150
- How it behaves:
151
-
152
- - You reported `true` → the OS back button fires your `onBack` (go back one
153
- internal page). Nothing else happens — the app stays on Home.
154
- - You reported `false` (or never adopted this API) → the OS back button is
155
- ignored and the user stays on Home. **No regression if you skip it.**
156
-
157
- Keep `reportNavigation` in sync with your router: the app caches the last value
158
- and reads it the instant back is pressed. A single choke point (call it at the
159
- end of your `render`/route function) is the simplest way to never miss a change.
160
- See `examples/client_demo/src/main.ts` for a worked home↔device example.
161
-
162
- ### Key catalog
163
-
164
- These are the keys you can subscribe to / send. Payloads are fully typed in
165
- TypeScript; the table is the quick reference (see the
166
- [README](../README.md#key-catalog-strictly-typed) for exact shapes).
167
-
168
- | Key | Payload (typed) | Direction |
169
- |---|---|---|
170
- | `userDevice` | device config / telemetry / user config arrays | native → you (also send to refresh) |
171
- | `deviceConnection` | `{ serialNo, isConnect }` | native → you (status) |
172
- | `deviceNotification` | `{ serialNo, isConnect, message, messageCode, isError }` | native → you |
173
- | `deviceLightSensors` | `{ serialNo, type, data }` — union on `type` (`'actlumus'` \| `'actlumus2'`); `data` has the spectrum plus the raw `light_sensor_*` channels | native → you |
174
- | `realTime` | `{ serialNo, success, value }` | both (command + state) |
175
- | `syncTime` | `{ serialNo, success }` | both |
176
- | `sensorLog` | paginated sensor log envelope | native → you |
177
- | `systemStatus` | `{ bluetooth, internet, permission, hasDevices, missingOptionalPermissions, missingEssentialPermissions, deviceSyncError }` | native → you (env status) |
178
- | `deleteMemory` | `{ serialNo, success }` | both |
179
- | `pingPong` | `{ data: 'ping' \| 'pong' }` | both |
180
- | `navigationBack` | `{}` (OS back pressed — via `onBack`) | native → you |
181
- | `language` | `{ deviceLocale, appLanguage, resolvedLanguage }` | native → you (language state) |
182
-
183
- > **System back button.** Use the `reportNavigation(canGoBack)` /
184
- > `onBack(cb)` helpers (see [Handle the system back button](#handle-the-system-back-button))
185
- > rather than subscribing to `navigationBack` directly — `reportNavigation`
186
- > sends the `navigationState` command the app needs to route the back button.
187
-
188
- **Permissions on `systemStatus`.** Two arrays of `AppPermission` names let you
189
- render permission prompts from your own UI:
190
-
191
- - `missingEssentialPermissions` — **required** for the app to work; background
192
- collection stays off until granted. Render a **blocking "Required permissions"
193
- card** (e.g. `["bluetoothScan", "bluetoothConnect"]`).
194
- - `missingOptionalPermissions` — **recommended** (help background reliability);
195
- render a **dismissible "Recommended permissions" nudge** (e.g.
196
- `["notification", "backgroundLocation"]`).
197
-
198
- Both are empty when nothing is missing. For the **required** card, wire the
199
- "Grant" button to `BridgeClient.requestPermission(name)` — it fires the **native
200
- in-app permission dialog** for that `AppPermission` (falls back to Settings only
201
- when the OS refuses the prompt). Pass a name straight from
202
- `missingEssentialPermissions` (e.g. `'bluetoothScan'`). There is no dedicated
203
- result key: the native side re-emits `systemStatus` once granted, so the card
204
- disappears when `missingEssentialPermissions` empties. `openSettings` remains
205
- available for the optional nudge (permissions not grantable by prompt on modern
206
- Android). See `examples/client_demo/src/render-home.ts` for a worked render.
207
-
208
- ### Translate your bundle — the `language` key
209
-
210
- The app tells you **two** languages, and the distinction between them is the
211
- whole point of the channel:
212
-
213
- ```typescript
214
- BridgeClient.on(BRIDGE_KEYS.language, ({ deviceLocale, appLanguage, resolvedLanguage }) => {
215
- // Your precedence rule. This one honours an explicit choice, else the device.
216
- const lang = appLanguage ?? pickSupported(deviceLocale) ?? 'en';
217
- renderIn(lang);
218
- });
219
- ```
220
-
221
- - **`deviceLocale`** — the raw device locale as a BCP-47 tag (`"pt-BR"`,
222
- `"es-419"`, `"ja-JP"`), exactly as the OS reports it, region included.
223
- - **`appLanguage`** — the language the user explicitly picked in the app
224
- Settings (`"en" | "pt" | "de" | "fr"`), or **`null`** when they never picked
225
- one.
226
- - **`resolvedLanguage`** — the language the native chrome (splash, Settings,
227
- loading/error screens) is rendering right now.
228
-
229
- **Why two raw values instead of one answer.** The app itself ships four
230
- languages and collapses everything else to English. Your bundle has no such
231
- limit — it may ship Spanish, Italian, Japanese. If we sent you only the resolved
232
- language, a device set to Spanish would reach you as `en` and you would render
233
- English while holding a Spanish bundle. The information would have been
234
- destroyed on our side before reaching you. So we send both and let you decide:
235
-
236
- | `deviceLocale` | `appLanguage` | a reasonable decision |
237
- |---|---|---|
238
- | `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 |
239
- | `es-ES` | `"pt"` | render **Portuguese** — an explicit choice outranks the device |
240
- | `pt-BR` | `null` | render Portuguese |
241
- | `ja-JP` | `"de"` | render German (the explicit choice), or Japanese if you'd rather honour the device — your call, with both values in hand |
242
-
243
- `appLanguage` is `null` when — and only when — the user never chose. It is never
244
- a fallback in disguise, so `null` is a real signal you can branch on. Use
245
- `resolvedLanguage` when you'd rather stay visually consistent with the native
246
- frame around your page than follow the device.
247
-
248
- **When it arrives — and how to ask for it.** The app pushes this key on attach,
249
- again right after the handshake ack, and on every change in the app Settings, so
250
- a page already loaded re-renders without a restart. But the first push happens
251
- while the WebView is still being created, before your bundle exists, so **send
252
- the `language` command once after your handshake** to pull the current state:
253
-
254
- ```typescript
255
- BridgeClient.send('language', {}); // replies with a `language` event
256
- ```
257
-
258
- Without that pull, a first launch would show your default language until the
259
- user changed it in Settings. Subscribe first, then pull.
260
-
261
- Do not read `navigator.language` instead: it follows the system, not the in-app
262
- choice, and you would end up in Spanish while the native chrome around you is in
263
- Portuguese.
264
-
265
- **Older app versions.** There is no version negotiation. An app older than
266
- native `0.14.0` never emits this key, so the event simply never arrives — keep a
267
- default language for that case, and read `nativeVersion` from the handshake ack
268
- if you need to branch on it explicitly.
269
-
270
- ---
271
-
272
- ## 4. Develop in your browser (no app needed)
273
-
274
- Build your page, then serve it with the harness:
275
-
276
- ```bash
277
- npx condor-mobile-bridge dev -i dist/index.html
278
- ```
279
-
280
- Open `http://localhost:5173`. Your page runs inside the same sandboxed iframe,
281
- shell, policy and CSP as in the app, and a simulated app answers your commands.
282
- Rebuild and reload — the harness re-reads the file.
283
-
284
- Pick a **scenario** in the selector at the top (or with `--scenario`) to see
285
- what the real app does to your page:
286
-
287
- - `cold-start` — the first device requests are lost while the app starts;
288
- - `no-devices` — an account with no device: the device list never comes;
289
- - `shared-serial` — two devices with the same serial number;
290
- - `offline` — no internet, history unavailable.
291
-
292
- Your page is ready for a phone when it behaves in all of them. The page's
293
- console (`[condor dev]` lines) shows each command, reply and dropped request,
294
- and the dev panel in the corner lets you emit any event by hand.
295
-
296
- > Opening `dist/index.html` directly in a browser does not work: there is no
297
- > shell around it, so `BridgeClient.handshake()` rejects at once with a message
298
- > pointing to the harness.
299
-
300
- The [`examples/minimal-home`](../examples/minimal-home) example runs this way
301
- out of the box.
302
-
303
- ---
304
-
305
- ## 5. Bundle format & constraints
306
-
307
- Your final artifact is a `.zip` containing exactly **one self-contained
308
- `index.html`** at the root. Why the constraints:
309
-
310
- - The app injects your HTML via `iframe.srcdoc` with origin `null` and no base
311
- URL → **relative references don't resolve** (`<script src="app.js">`,
312
- `<img src="logo.png">` would break).
313
- - The app's Content-Security-Policy **blocks external fetches** → CDN scripts,
314
- Google Fonts, remote APIs called at load time won't work.
315
-
316
- So the bundle must have:
317
- - ✅ One `index.html` at the zip root.
318
- - ✅ JS and CSS **inlined** into the HTML.
319
- - ✅ Images/icons/fonts as **`data:` URIs**.
320
- - ✅ Total size **≤ 2 MB** (zipped). Note base64 inflates media ~33%.
321
- - ❌ No external `https://` script/style/font references loaded at runtime.
322
- - ❌ No code-splitting / dynamic `import()` across chunks (see below).
323
-
324
- ### Single-chunk build
325
-
326
- The bundler CLI is post-build and assumes **one JS + one CSS + assets**. If
327
- your framework code-splits, the split chunks won't resolve in the sandbox.
328
- Configure a single-file build:
329
-
330
- - **Vite:** add [`vite-plugin-singlefile`](https://github.com/richardtallent/vite-plugin-singlefile),
331
- or set `build.rollupOptions.output.inlineDynamicImports: true` +
332
- `manualChunks: undefined`.
333
- - **Create React App:** avoid `React.lazy` / dynamic `import()` in routes so the
334
- build emits a single bundle.
335
- - **esbuild:** bundle to a single IIFE/ESM file (see the example's `build.mjs`).
336
-
337
- ---
338
-
339
- ## 6. Build the bundle
340
-
341
- Once your build produces a `dist/index.html` (referencing a local `main.js`,
342
- CSS, images), run the CLI to inline everything and pack the zip:
343
-
344
- ```bash
345
- npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
346
- ```
347
-
348
- It inlines `<script src>`, `<link rel=stylesheet>`, CSS `url()`, and local
349
- `<img>`/icons into a single `index.html`, then zips it. Output:
350
-
351
- ```
352
- Built dist/bundle.zip
353
- entry: index.html (123.4 KB self-contained)
354
- bundleSize (zip): 45678 bytes
355
- bundleHash (zip): sha256-<hex>
356
- ```
357
-
358
- | Option | Description |
359
- |---|---|
360
- | `-i, --html <path>` | Input HTML. Required. |
361
- | `-o, --out <path>` | Output `.zip` (default `dist/whitelabel-bundle.zip`). |
362
- | `--max-bytes <n>` | Warning threshold (default 2 MB). |
363
-
364
- The hash is deterministic — the same input always yields the same
365
- `bundleHash`. The CLI **warns** above 2 MB but the **app rejects** oversized
366
- bundles and falls back to a default screen, so keep under the limit.
367
-
368
- ---
369
-
370
- ## 7. Deliver
371
-
372
- Publish it yourself with a manager user of your company —
373
- `npx condor-mobile-bridge canvas publish dist/index.html --version 1.0.0` — or
374
- send the resulting `dist/bundle.zip` through the publication channel Condor
375
- gave you (the admin upload). The backend validates structure, self-containment,
376
- and size on upload, and records the `bundleSize`/`bundleHash`. Once published,
377
- the app downloads, verifies, and renders your Home — for **every user of your
378
- company**, so test on a development test company first. See
379
- [Testing on a device](./device-testing.md).
380
-
381
- ---
382
-
383
- ## 8. Troubleshooting
384
-
385
- | Symptom | Likely cause |
386
- |---|---|
387
- | Blank screen / nothing loads | Relative `<script src>` or external URL — must be inlined. Run the build CLI. |
388
- | Images missing | Referenced by path instead of `data:` URI — the CLI inlines local images; remote images are blocked. |
389
- | 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. |
390
- | Bundle rejected on upload / falls back | Over 2 MB, multiple files at root, or not self-contained. |
391
- | `handshake` rejects in the browser | The page is not inside the shell. Serve it with `npx condor-mobile-bridge dev`. |
392
- | Stuck on "loading devices" on the phone | The device request was lost or the account has no device. Use `requestUserDevices()` and test the `cold-start` and `no-devices` scenarios. |
393
- | A command does nothing | The command may not be allowed by the app policy; it's dropped silently. |
394
-
395
- ---
396
-
397
- ## Next steps
398
-
399
- - Copy [`examples/minimal-home`](../examples/minimal-home) and start from there.
400
- - Full API reference: the [README](../README.md).
1
+ # Build your Canvas Home — start here
2
+
3
+ You build the Home of the Condor Canvas app as an ordinary web page: one
4
+ self-contained `index.html`, at most 2 MB zipped. The app downloads it, checks
5
+ it, and shows it inside a sandboxed iframe after the person logs in. Your page
6
+ talks to the app — device list, live light readings, history, permissions,
7
+ language — through `BridgeClient` from this SDK. No Flutter, Android or iOS
8
+ knowledge is needed.
9
+
10
+ ```
11
+ ┌─────────────────────────────┐
12
+ │ Condor Canvas app (native) │
13
+ │ ┌──────────────────────┐ │
14
+ │ │ iframe: your bundle │ │
15
+ │ │ BridgeClient ◄────────────► devices, Bluetooth, server
16
+ │ └──────────────────────┘ │
17
+ └─────────────────────────────┘
18
+ ```
19
+
20
+ The documentation is written once, for people and for AI agents alike:
21
+
22
+ 1. [`AGENTS.md`](../AGENTS.md) — **read this first**: the rules, the boot
23
+ sequence to copy, the browser harness and what "done" means.
24
+ 2. [`docs/bridge-contract.md`](./bridge-contract.md) — what each key and command
25
+ does, including the `language` key and the back button.
26
+ 3. [`docs/device-testing.md`](./device-testing.md) — testing in the browser and
27
+ on a phone, and publishing from the command line.
28
+ 4. [`docs/pilot-onboarding.md`](./pilot-onboarding.md) — the steps from zero to
29
+ a bundle live for your company.
30
+ 5. [`docs/troubleshooting.md`](./troubleshooting.md) when something does not
31
+ work, and [`docs/limits.md`](./limits.md) for every hard number.
32
+
33
+ A complete, working Home to start from ships in the package:
34
+ `examples/minimal-home/`.