@condorinst/mobile-bridge 0.20.0 → 0.21.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 +100 -42
- package/CHANGELOG.md +76 -0
- package/README.md +64 -14
- package/contract/bridge-contract.json +57 -13
- package/dist/cli.cjs +436 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +433 -3
- package/dist/cli.js.map +1 -1
- package/dist/dev-harness.global.js +2110 -0
- package/dist/dev.cjs +1662 -0
- package/dist/dev.cjs.map +1 -0
- package/dist/dev.d.cts +683 -0
- package/dist/dev.d.ts +683 -0
- package/dist/dev.js +1611 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.cjs +96 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +236 -14
- package/dist/index.d.ts +236 -14
- package/dist/index.js +94 -3
- package/dist/index.js.map +1 -1
- package/docs/agent-guide.md +75 -24
- package/docs/bridge-contract.md +121 -20
- package/docs/client-guide.md +39 -28
- package/docs/device-testing.md +84 -0
- package/docs/pilot-onboarding.md +26 -12
- package/docs/troubleshooting.md +79 -18
- package/examples/minimal-home/AGENTS.md +9 -6
- package/examples/minimal-home/README.md +10 -5
- package/examples/minimal-home/build.mjs +2 -1
- package/examples/minimal-home/index.html +2 -0
- package/examples/minimal-home/package.json +3 -2
- package/examples/minimal-home/src/main.ts +151 -73
- package/llms-full.txt +2041 -0
- package/llms.txt +15 -13
- package/package.json +9 -3
package/docs/agent-guide.md
CHANGED
|
@@ -19,33 +19,55 @@ in memory until the next reload.
|
|
|
19
19
|
|
|
20
20
|
## Stage 0 — set up so you can see what you are doing
|
|
21
21
|
|
|
22
|
+
Start from `examples/minimal-home/` — a complete, working client with the
|
|
23
|
+
reference boot sequence, and a build script that produces a single-file bundle.
|
|
24
|
+
Copy it first, then install, so its `package.json` is the one you keep:
|
|
25
|
+
|
|
22
26
|
```bash
|
|
23
27
|
mkdir my-home && cd my-home
|
|
24
|
-
npm
|
|
25
|
-
|
|
26
|
-
npm install
|
|
28
|
+
npm pack @condorinst/mobile-bridge@^0.21.0 && tar -xzf condorinst-mobile-bridge-*.tgz
|
|
29
|
+
cp -r package/examples/minimal-home/. . && rm -rf package condorinst-mobile-bridge-*.tgz
|
|
30
|
+
npm install
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
Or, in a project you already have: `npm install @condorinst/mobile-bridge@^0.21.0`
|
|
34
|
+
and copy only `src/main.ts`, `build.mjs` and `index.html` from
|
|
35
|
+
`node_modules/@condorinst/mobile-bridge/examples/minimal-home/`.
|
|
36
|
+
|
|
37
|
+
Pin the SDK to the minor you are building against. Below `1.0.0`, a minor can
|
|
38
|
+
change the contract — `^0.21.0` gives you patches only, which is what you want.
|
|
39
|
+
|
|
40
|
+
**Run it in the harness**, not in a bare browser tab:
|
|
32
41
|
|
|
33
42
|
```bash
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
npm run build
|
|
44
|
+
npx condor-mobile-bridge dev -i dist/index.html --scenario default
|
|
36
45
|
```
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
The harness serves your bundle inside the same sandboxed iframe, shell, policy
|
|
48
|
+
and CSP as the app, with a simulated native side behind it, and a dev panel to
|
|
49
|
+
emit events by hand. It re-reads the file on every reload. Opening
|
|
50
|
+
`dist/index.html` directly does **not** work: there is no shell, so
|
|
51
|
+
`BridgeClient.handshake()` rejects at once.
|
|
52
|
+
|
|
53
|
+
The scenarios reproduce what the device does to you — switch with the selector
|
|
54
|
+
at the top or `--scenario`:
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
the
|
|
56
|
+
| Scenario | Your bundle must… |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `default` | render two devices |
|
|
59
|
+
| `cold-start` | leave the loading screen although the first `userDevice` requests are lost |
|
|
60
|
+
| `no-devices` | show an empty state when `userDevice` never answers |
|
|
61
|
+
| `shared-serial` | keep two devices with the same `serialNo` apart |
|
|
62
|
+
| `offline` | explain that history is unavailable without internet |
|
|
45
63
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
Add `--native-version null` (or an older version such as `0.15.0`) to test
|
|
65
|
+
your capability branches, and use the **◀ OS back** button at the top to press
|
|
66
|
+
the OS back button.
|
|
67
|
+
|
|
68
|
+
If you already have a dev server (Vite), mount the same harness from your own
|
|
69
|
+
page with `createDevHarness()` from `@condorinst/mobile-bridge/dev` — in the
|
|
70
|
+
host page only, never in the bundle (`validate` rejects that).
|
|
49
71
|
|
|
50
72
|
## Stage 1 — render before you connect
|
|
51
73
|
|
|
@@ -72,9 +94,9 @@ const canQueryTimeRange = has('0.15.0');
|
|
|
72
94
|
const hasLanguageKey = has('0.14.0');
|
|
73
95
|
```
|
|
74
96
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
as versions.
|
|
97
|
+
Or use the SDK's `versionAtLeast(ack.nativeVersion, '0.16.0')`, which does
|
|
98
|
+
exactly that and returns `false` for `null`. Do not compare version strings
|
|
99
|
+
lexically — `'0.9.0' > '0.16.0'` is `true` as strings and false as versions.
|
|
78
100
|
|
|
79
101
|
**Do not branch on `ack.accepted`.** It is always `true`; the native side never
|
|
80
102
|
refuses a handshake. Anything you gate on it is ungated.
|
|
@@ -89,7 +111,27 @@ BridgeClient.on(BRIDGE_KEYS.userDevice, (p) => { setDevices(p); render(); });
|
|
|
89
111
|
BridgeClient.on(BRIDGE_KEYS.systemStatus, (s) => { setStatus(s); render(); });
|
|
90
112
|
BridgeClient.on(BRIDGE_KEYS.deviceConnection, (c) => { setConnected(c); render(); });
|
|
91
113
|
|
|
92
|
-
|
|
114
|
+
const result = await BridgeClient.requestUserDevices();
|
|
115
|
+
if (result.status === 'timeout') renderEmptyState(result.hasDevices === false);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Do not replace the helper with `send('userDevice')`.** The device list is
|
|
119
|
+
served by the app's background service, which starts after your bundle; a
|
|
120
|
+
request sent before it is up is lost, and an account with no device never gets
|
|
121
|
+
a reply. The helper resends with backoff for 30 s and then tells you to show the
|
|
122
|
+
empty state. Keep the `userDevice` subscription: roster changes arrive on it
|
|
123
|
+
later without a request. `docs/troubleshooting.md` has the details.
|
|
124
|
+
|
|
125
|
+
Read device data from the typed items: the list is `devicesConfig`, and the
|
|
126
|
+
name, battery and last sync are in `devicesTelemetry` — join them on
|
|
127
|
+
`deviceId`, never on `serialNo` alone.
|
|
128
|
+
|
|
129
|
+
Then follow the connection state of each device:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
for (const d of result.devices.devicesConfig) {
|
|
133
|
+
BridgeClient.send('deviceConnection', { type: 'add', serialNo: d.serialNo, deviceId: d.deviceId });
|
|
134
|
+
}
|
|
93
135
|
```
|
|
94
136
|
|
|
95
137
|
`on` returns an unsubscribe function. If your UI has views that mount and
|
|
@@ -157,7 +199,11 @@ Timestamps are **device-local**. Build ranges with local `Date` getters, never
|
|
|
157
199
|
`toISOString()` — converting to UTC shifts every series by the timezone offset
|
|
158
200
|
and produces data that looks entirely plausible.
|
|
159
201
|
|
|
160
|
-
Paginate with the cursors exactly as received. They are opaque.
|
|
202
|
+
Paginate with the cursors exactly as received. They are opaque. Without a range
|
|
203
|
+
the first page is the **most recent** one (go older with `endingBefore`); with
|
|
204
|
+
`startTime` it is the **oldest** of the range (go newer with `startingAfter`).
|
|
205
|
+
A device writes one log per period — programmable, 60 s by default — so a week
|
|
206
|
+
is about 10 000 logs: use `limit: 1000` and do not assume the cadence.
|
|
161
207
|
|
|
162
208
|
## Stage 7 — back button
|
|
163
209
|
|
|
@@ -201,13 +247,18 @@ necessary, not sufficient.
|
|
|
201
247
|
repeats on the account also carries its `deviceId` (`nativeVersion` ≥ 0.17.0).
|
|
202
248
|
- [ ] Errors are reported through `BridgeClient.send('jsError', …)`, the only
|
|
203
249
|
channel out.
|
|
204
|
-
- [ ]
|
|
250
|
+
- [ ] Every harness scenario passes: `default`, `cold-start`, `no-devices`,
|
|
251
|
+
`shared-serial`, `offline`.
|
|
252
|
+
- [ ] The `nativeVersion === null` branch renders something sensible.
|
|
205
253
|
|
|
206
254
|
## Delivery
|
|
207
255
|
|
|
208
256
|
Hand Condor the `.zip` along with its size and `sha256-<hex>` hash, both printed
|
|
209
257
|
by the `build` command. The app verifies the hash against the manifest and
|
|
210
|
-
rejects any mismatch.
|
|
258
|
+
rejects any mismatch. Condor publishes it for your company, which reaches every
|
|
259
|
+
user of that company — test on a development test company first.
|
|
260
|
+
`docs/device-testing.md` explains the path to the phone and how to inspect the
|
|
261
|
+
bundle there; `docs/pilot-onboarding.md` has the full checklist.
|
|
211
262
|
|
|
212
263
|
## When something does not work
|
|
213
264
|
|
package/docs/bridge-contract.md
CHANGED
|
@@ -32,7 +32,7 @@ Four numbers coexist. Only one tells you what the device can do.
|
|
|
32
32
|
| `nativeVersion` | `0.17.0` | **The capability signal.** The bridge schema the app on this device implements. |
|
|
33
33
|
| `shellVersion` | `0.6.0` | The Condor shell wrapping your iframe. |
|
|
34
34
|
| client protocol | `0.5.0` | The postMessage protocol `BridgeClient` speaks. |
|
|
35
|
-
| package | `0.
|
|
35
|
+
| package | `0.21.0` | The npm version you compiled against. |
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
38
|
const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
|
|
@@ -44,11 +44,11 @@ Two traps:
|
|
|
44
44
|
**`accepted` is always `true`.** There is no version negotiation and the native
|
|
45
45
|
side never refuses. Branching on it is branching on a constant.
|
|
46
46
|
|
|
47
|
-
**`nativeVersion` can be `null`.**
|
|
48
|
-
bundles compiled against SDK ≤ 0.16.x, where the shell did not
|
|
49
|
-
`null` as "assume only the baseline" and keep a working
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
**`nativeVersion` can be `null`.** When the shell could not reach the native
|
|
48
|
+
side, and in bundles compiled against SDK ≤ 0.16.x, where the shell did not
|
|
49
|
+
relay it. Treat `null` as "assume only the baseline" and keep a working
|
|
50
|
+
default. The dev harness reports `0.17.0` by default; `--native-version null`
|
|
51
|
+
exercises this path.
|
|
52
52
|
|
|
53
53
|
### Capability matrix
|
|
54
54
|
|
|
@@ -70,8 +70,8 @@ with `BridgeClient.on(key, cb)`, which returns an unsubscribe function.
|
|
|
70
70
|
|
|
71
71
|
| Key | Payload type | Emitted when |
|
|
72
72
|
| --- | --- | --- |
|
|
73
|
-
| `pingPong` | `PingPongPayload` | reply to
|
|
74
|
-
| `userDevice` | `UserDevicePayload` | reply to `userDevice
|
|
73
|
+
| `pingPong` | `PingPongPayload` | reply to `ping`, once the app's background service is up |
|
|
74
|
+
| `userDevice` | `UserDevicePayload` | reply to `userDevice` **when the list loads** (not guaranteed — see below), and on roster changes |
|
|
75
75
|
| `deviceLightSensors` | `DeviceLightSensorsPayload` | real-time stream, only while `realTime` is on for that device |
|
|
76
76
|
| `deviceConnection` | `DeviceConnectionPayload` | a device connects or disconnects |
|
|
77
77
|
| `deviceNotification` | `DeviceNotificationPayload` | device-level notice, including errors |
|
|
@@ -125,6 +125,52 @@ conclusions before.
|
|
|
125
125
|
off. Render a blocking card; wire its button to
|
|
126
126
|
`BridgeClient.requestPermission(name)` with a name taken from that array.
|
|
127
127
|
|
|
128
|
+
**`hasDevices` is a hint.** It is `true` only after the app loaded a non-empty
|
|
129
|
+
device list from the server. `false` means "no linked device" **or** "the app
|
|
130
|
+
could not ask" (no internet, session problem). Do not show "you have no device"
|
|
131
|
+
on `false` alone; see the next section.
|
|
132
|
+
|
|
133
|
+
### `userDevice` in detail
|
|
134
|
+
|
|
135
|
+
**It is not guaranteed to answer.** `userDevice` (and `ping`) are handled by
|
|
136
|
+
the app's background service, which starts after your bundle is running; a
|
|
137
|
+
request sent before it is up is lost. And the app replies only when the list
|
|
138
|
+
loads: an account with no linked device, or a failed request, gets **no reply
|
|
139
|
+
at all** — never an empty list. Use the helper, which resends with backoff and
|
|
140
|
+
tells you when to give up:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const r = await BridgeClient.requestUserDevices(); // getFromServer: false
|
|
144
|
+
// { status: 'ok', devices } | { status: 'timeout', hasDevices: boolean | null }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**`getFromServer`.** `false` (default) answers from the app's memory or cache
|
|
148
|
+
while it is fresh, and asks the server otherwise — use it at boot. `true` asks
|
|
149
|
+
the server first and falls back to the cache if that fails — use it for a
|
|
150
|
+
"Refresh" button.
|
|
151
|
+
|
|
152
|
+
**Shape.** Three arrays, typed as `DeviceConfigItem`, `DeviceTelemetryItem` and
|
|
153
|
+
`DeviceUserConfigItem` in `dist/index.d.ts`:
|
|
154
|
+
|
|
155
|
+
| Array | Holds | Notable fields |
|
|
156
|
+
| --- | --- | --- |
|
|
157
|
+
| `devicesConfig` | one item per linked device — **the list to render** | `deviceId`, `serialNo`, `modelId` (`'actlumus'`, `'actlumus_plus'`; absent = ActLumus), `deviceBodyPart` (`'wrist'` \| `'necklace'` \| `null`), `bleConnectionPeriod` (minutes) |
|
|
158
|
+
| `devicesTelemetry` | last state read from each device | `deviceName`, `batteryLevel` (0–100 %), `memoryDataUsage` (0–100 %), `updataDateTime` (epoch ms, last sync — the misspelling is the real name), `deviceVersion`, `isCharging`, `isOnDock`, `syncErrors[]` |
|
|
159
|
+
| `devicesUserConfig` | per-device toggles | `syncTime`, `realTime`, `deleteMemory`, `readAllMemory`, `readAllSystemLog` |
|
|
160
|
+
|
|
161
|
+
The name and battery of a device are in **telemetry**, not in the config. A
|
|
162
|
+
device that never synced may have no telemetry item, or one with every reading
|
|
163
|
+
`null`: render it anyway.
|
|
164
|
+
|
|
165
|
+
**Joining the arrays.** Join on `deviceId`. An item of `devicesTelemetry` or
|
|
166
|
+
`devicesUserConfig` without `deviceId` is legacy data: fall back to `serialNo`
|
|
167
|
+
only when that `serialNo` appears once in `devicesConfig`.
|
|
168
|
+
|
|
169
|
+
**Sync errors.** `devicesTelemetry[].syncErrors` is the per-device history
|
|
170
|
+
(`attemptDate`, `message`, `messageCode`); `systemStatus.deviceSyncErrorById`
|
|
171
|
+
is the current error per device, already localized. Show the current one; use
|
|
172
|
+
the history for detail screens.
|
|
173
|
+
|
|
128
174
|
## Commands
|
|
129
175
|
|
|
130
176
|
`BridgeClient.send(command, args)` is **fire-and-forget**: no return value, no
|
|
@@ -136,7 +182,7 @@ column.
|
|
|
136
182
|
| `ping` | — | no | `pingPong` |
|
|
137
183
|
| `userDevice` | `{ getFromServer?: boolean }` | no | `userDevice` |
|
|
138
184
|
| `realTime` | `{ serialNo: number, deviceId?: number, value: boolean }` | no | `realTime` |
|
|
139
|
-
| `deviceConnection` | `{ type: 'add' \| 'remove', serialNo: number, deviceId?: number }` | no | `deviceConnection` |
|
|
185
|
+
| `deviceConnection` | `{ type: 'add' \| 'remove', serialNo: number, deviceId?: number }` — subscribe to / stop following one device's connection state | no | `deviceConnection` |
|
|
140
186
|
| `sensorLog` | see below | no | `sensorLog` |
|
|
141
187
|
| `openSettings` | — | no | — |
|
|
142
188
|
| `requestPermission` | `{ permission: string }` | no | — (re-emits `systemStatus`) |
|
|
@@ -152,6 +198,11 @@ column.
|
|
|
152
198
|
`serialNo` is an integer. Passing a string makes the native side drop the
|
|
153
199
|
message in silence.
|
|
154
200
|
|
|
201
|
+
`deviceConnection` **pairs nothing.** `type: 'add'` asks the app to report the
|
|
202
|
+
BLE connection state of a device you already have from `userDevice`; the state
|
|
203
|
+
then arrives on the `deviceConnection` key whenever it changes. `'remove'` stops
|
|
204
|
+
it. Send `add` for each device after the list arrives.
|
|
205
|
+
|
|
155
206
|
### Two devices with the same `serialNo`
|
|
156
207
|
|
|
157
208
|
An ActLumus and an ActLumus Plus have separate serial sequences, so one account
|
|
@@ -192,6 +243,8 @@ timeout on any spinner.
|
|
|
192
243
|
Prefer these over raw `send` — they add correlation, timeouts and types:
|
|
193
244
|
|
|
194
245
|
```ts
|
|
246
|
+
BridgeClient.requestUserDevices({ getFromServer }) // resends until the list arrives, 30 s default
|
|
247
|
+
BridgeClient.waitForService() // resends ping until the background service answers
|
|
195
248
|
BridgeClient.fetchSensorLog(query, { timeoutMs }) // correlates requestId, 10 s default
|
|
196
249
|
BridgeClient.requestPermission(name)
|
|
197
250
|
BridgeClient.reportNavigation(canGoBack)
|
|
@@ -225,9 +278,48 @@ series by the timezone offset, producing plausible data and no error at all.
|
|
|
225
278
|
**Cursors are opaque.** `startingAfter` and `endingBefore` are mutually
|
|
226
279
|
exclusive; echo back exactly what you received and never construct one.
|
|
227
280
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
281
|
+
**Sweeping a period.** Keep `startTime` and `endTime` on **every** page and add
|
|
282
|
+
the cursor; the cursors are valid only inside the range they came from:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
const logs: SensorLogItem[] = [];
|
|
286
|
+
let startingAfter: string | undefined;
|
|
287
|
+
for (;;) {
|
|
288
|
+
const page = await BridgeClient.fetchSensorLog({
|
|
289
|
+
requestId: crypto.randomUUID(), serialNo, limit: 1000,
|
|
290
|
+
startTime: '2026-10-13 00:00:00', endTime: '2026-10-20 00:00:00', // local time
|
|
291
|
+
...(startingAfter ? { startingAfter } : {}),
|
|
292
|
+
});
|
|
293
|
+
if (!page.success) break; // see the error codes below
|
|
294
|
+
logs.push(...(page.logs ?? []));
|
|
295
|
+
if (!page.hasMore || !page.startingAfter) break;
|
|
296
|
+
startingAfter = page.startingAfter;
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Which page comes first.** `logs` is always chronological ascending inside a
|
|
301
|
+
page. Which page you get first depends on the query:
|
|
302
|
+
|
|
303
|
+
| Query | First page | Next page |
|
|
304
|
+
| --- | --- | --- |
|
|
305
|
+
| no cursor, no range | the **most recent** `limit` logs | older: `endingBefore` = the reply's `endingBefore`, while `hasPrevious` |
|
|
306
|
+
| `startTime` (with or without `endTime`) | the **oldest** logs of the range | newer: `startingAfter` = the reply's `startingAfter`, while `hasMore` |
|
|
307
|
+
| only `endTime` | the most recent logs before it | older, as in the first row |
|
|
308
|
+
|
|
309
|
+
`limit` defaults to 10. With a time range, `hasMore` and `hasPrevious` are
|
|
310
|
+
computed *within* the period, so `hasMore: false` means "this period is
|
|
311
|
+
complete", not "no more data exists".
|
|
312
|
+
|
|
313
|
+
**How many logs to expect.** One per log period, which is **programmable per
|
|
314
|
+
device** (default 60 s, so about 1 440 per day). Do not hard-code it: derive the
|
|
315
|
+
period from consecutive `log_time` values, and page with `limit` up to 1000.
|
|
316
|
+
Pages depend on the previous cursor, so they cannot be fetched in parallel.
|
|
317
|
+
|
|
318
|
+
**Fields.** Each log is a `SensorLogItem`: `log_time`, `body_part`,
|
|
319
|
+
`temperature` (°C), `lux_photopic` and `lux_melanopic` (lux), the raw light
|
|
320
|
+
channels and the activity counters. `null` on a channel means the hardware does
|
|
321
|
+
not have it, never zero. `device_log_time` and `app_log_time` mark the upload
|
|
322
|
+
batch, not the log — do not plot them.
|
|
231
323
|
|
|
232
324
|
## Error shapes
|
|
233
325
|
|
|
@@ -237,11 +329,15 @@ There are three, and none of them is a thrown exception.
|
|
|
237
329
|
`no-confirmation-handler`, `confirmation-error`, `user-rejected`, `not-allowed`.
|
|
238
330
|
Visible only as `console.warn`.
|
|
239
331
|
|
|
240
|
-
**2. `sensorLog` failure** — `{ requestId, success: false, error }`. The
|
|
241
|
-
are `CONFLICTING_CURSORS`, `INVALID_BODY_PART`, `INVALID_LIMIT`,
|
|
332
|
+
**2. `sensorLog` failure** — `{ requestId, success: false, error }`. The
|
|
333
|
+
contract codes are `CONFLICTING_CURSORS`, `INVALID_BODY_PART`, `INVALID_LIMIT`,
|
|
242
334
|
`INVALID_START_TIME`, `INVALID_END_TIME`, `INVALID_TIME_RANGE`,
|
|
243
|
-
`MISSING_DEVICE_SERIAL_NO`, `invalid session
|
|
244
|
-
`
|
|
335
|
+
`MISSING_DEVICE_SERIAL_NO`, `DEVICE_SERIAL_AMBIGUOUS`, `invalid session` and
|
|
336
|
+
`device not found for this user`. Some carry a suffix (`INVALID_LIMIT: 5000`),
|
|
337
|
+
so **match by prefix**, not equality. When the request never reached the
|
|
338
|
+
server (no internet, timeout), `error` is free text from the transport — so
|
|
339
|
+
treat any value outside the list as "history unavailable right now" and offer a
|
|
340
|
+
retry. `error` is a **diagnostic, never user-facing text** — map it to your own
|
|
245
341
|
copy.
|
|
246
342
|
|
|
247
343
|
**3. Silence** — nothing reaches JavaScript. This is the dominant failure mode.
|
|
@@ -252,12 +348,17 @@ The six paths that produce it are enumerated in `docs/troubleshooting.md`.
|
|
|
252
348
|
The app owns the OS back button and will delegate it to you only if you ask.
|
|
253
349
|
|
|
254
350
|
```ts
|
|
255
|
-
|
|
256
|
-
|
|
351
|
+
const stack: string[] = ['home']; // keep navigation in memory
|
|
352
|
+
function go(view: string) { stack.push(view); render(); BridgeClient.reportNavigation(stack.length > 1); }
|
|
353
|
+
BridgeClient.onBack(() => { stack.pop(); render(); BridgeClient.reportNavigation(stack.length > 1); });
|
|
257
354
|
```
|
|
258
355
|
|
|
259
|
-
|
|
260
|
-
|
|
356
|
+
Keep the navigation stack in memory rather than in the History API: the
|
|
357
|
+
bundle lives in a sandboxed `srcdoc` iframe, where `history.pushState` is not
|
|
358
|
+
something to rely on. Report `false` when you are at your root, and the app
|
|
359
|
+
handles back itself. Report state on every internal navigation, not once at
|
|
360
|
+
startup. In the harness, the **◀ OS back** button at the top presses the OS
|
|
361
|
+
back button for you.
|
|
261
362
|
|
|
262
363
|
## Status bar
|
|
263
364
|
|
package/docs/client-guide.md
CHANGED
|
@@ -61,13 +61,13 @@ Pin the version with a caret:
|
|
|
61
61
|
```jsonc
|
|
62
62
|
// package.json
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@condorinst/mobile-bridge": "^0.
|
|
64
|
+
"@condorinst/mobile-bridge": "^0.21.0"
|
|
65
65
|
}
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
**Updates.** The SDK is on `0.x` during the pilot. With `^0.
|
|
69
|
-
`npm install` (or your CI) auto-picks **patch** releases (`0.
|
|
70
|
-
no action needed. A `0.
|
|
68
|
+
**Updates.** The SDK is on `0.x` during the pilot. With `^0.21.0`, running
|
|
69
|
+
`npm install` (or your CI) auto-picks **patch** releases (`0.21.x`) — bug fixes,
|
|
70
|
+
no action needed. A `0.22.0` (a larger change during beta) is an intentional
|
|
71
71
|
opt-in: you bump the range when you're ready. `1.0.0` will mark the
|
|
72
72
|
production-stable contract.
|
|
73
73
|
|
|
@@ -112,7 +112,7 @@ const unsubscribe = BridgeClient.on(BRIDGE_KEYS.deviceConnection, (payload) => {
|
|
|
112
112
|
### Send commands (you → native)
|
|
113
113
|
|
|
114
114
|
```typescript
|
|
115
|
-
|
|
115
|
+
const devices = await BridgeClient.requestUserDevices(); // device list, resent until it arrives
|
|
116
116
|
BridgeClient.send('realTime', { serialNo: 1234, value: true }); // start real-time
|
|
117
117
|
BridgeClient.openSettings(); // open native Settings
|
|
118
118
|
BridgeClient.requestPermission('bluetoothScan'); // in-app permission dialog
|
|
@@ -120,6 +120,12 @@ BridgeClient.requestPermission('bluetoothScan'); // in-app permission
|
|
|
120
120
|
|
|
121
121
|
> Commands are gated by the app's policy. A command that isn't allowed is
|
|
122
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).
|
|
123
129
|
|
|
124
130
|
### Handle the system back button
|
|
125
131
|
|
|
@@ -265,32 +271,34 @@ if you need to branch on it explicitly.
|
|
|
265
271
|
|
|
266
272
|
## 4. Develop in your browser (no app needed)
|
|
267
273
|
|
|
268
|
-
|
|
269
|
-
auto-activates **mock mode**: the bridge no longer expects a native side, and
|
|
270
|
-
you get tooling to simulate it.
|
|
274
|
+
Build your page, then serve it with the harness:
|
|
271
275
|
|
|
272
|
-
```
|
|
273
|
-
|
|
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
|
-
}
|
|
276
|
+
```bash
|
|
277
|
+
npx condor-mobile-bridge dev -i dist/index.html
|
|
279
278
|
```
|
|
280
279
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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.
|
|
287
295
|
|
|
288
|
-
>
|
|
289
|
-
>
|
|
290
|
-
>
|
|
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.
|
|
291
299
|
|
|
292
|
-
The [`examples/minimal-home`](../examples/minimal-home) example
|
|
293
|
-
|
|
300
|
+
The [`examples/minimal-home`](../examples/minimal-home) example runs this way
|
|
301
|
+
out of the box.
|
|
294
302
|
|
|
295
303
|
---
|
|
296
304
|
|
|
@@ -364,7 +372,9 @@ bundles and falls back to a default screen, so keep under the limit.
|
|
|
364
372
|
Send the resulting `dist/bundle.zip` through the publication channel Condor
|
|
365
373
|
gave you (the admin upload). The backend validates structure, self-containment,
|
|
366
374
|
and size on upload, and records the `bundleSize`/`bundleHash`. Once published,
|
|
367
|
-
the app downloads, verifies, and renders your Home
|
|
375
|
+
the app downloads, verifies, and renders your Home — for **every user of your
|
|
376
|
+
company**, so test on a development test company first. See
|
|
377
|
+
[Testing on a device](./device-testing.md).
|
|
368
378
|
|
|
369
379
|
---
|
|
370
380
|
|
|
@@ -376,7 +386,8 @@ the app downloads, verifies, and renders your Home.
|
|
|
376
386
|
| Images missing | Referenced by path instead of `data:` URI — the CLI inlines local images; remote images are blocked. |
|
|
377
387
|
| 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
388
|
| Bundle rejected on upload / falls back | Over 2 MB, multiple files at root, or not self-contained. |
|
|
379
|
-
| `handshake`
|
|
389
|
+
| `handshake` rejects in the browser | The page is not inside the shell. Serve it with `npx condor-mobile-bridge dev`. |
|
|
390
|
+
| 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. |
|
|
380
391
|
| A command does nothing | The command may not be allowed by the app policy; it's dropped silently. |
|
|
381
392
|
|
|
382
393
|
---
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Testing on a device
|
|
2
|
+
|
|
3
|
+
The harness (`npx condor-mobile-bridge dev`) catches most problems in the
|
|
4
|
+
browser. This page covers the rest: how a bundle reaches a phone, what that
|
|
5
|
+
costs, and how to see what it is doing once it is there.
|
|
6
|
+
|
|
7
|
+
## How a bundle reaches the screen
|
|
8
|
+
|
|
9
|
+
You do not install a bundle on a phone. The app downloads it:
|
|
10
|
+
|
|
11
|
+
1. You deliver `bundle.zip`, its `bundleSize` and its `bundleHash` (printed by
|
|
12
|
+
`condor-mobile-bridge build`) to your Condor contact.
|
|
13
|
+
2. Condor publishes it for **your company**. There is exactly one current
|
|
14
|
+
bundle per company.
|
|
15
|
+
3. Each copy of the app asks for its company's manifest when the Home opens
|
|
16
|
+
and every time the app returns to the foreground. If the `bundleHash`
|
|
17
|
+
changed, it downloads the `.zip`, checks size and hash, and swaps the Home
|
|
18
|
+
without a restart. Otherwise it keeps the cached copy, also when offline.
|
|
19
|
+
|
|
20
|
+
Consequences:
|
|
21
|
+
|
|
22
|
+
- **Reinstalling the app changes nothing.** The bundle is not inside the app.
|
|
23
|
+
- **Publishing reaches every user of that company** the next time their app
|
|
24
|
+
comes to the foreground. There is no per-user or per-device preview today.
|
|
25
|
+
- **A rejected bundle is invisible to the person.** The app keeps its previous
|
|
26
|
+
bundle, or shows its default screen, if the download fails a gate — which is
|
|
27
|
+
why `validate --strict` must pass before you hand anything over.
|
|
28
|
+
|
|
29
|
+
## Development and production are separate worlds
|
|
30
|
+
|
|
31
|
+
Condor runs a **development** environment next to production. An app build
|
|
32
|
+
talks to exactly one of them, and accounts, devices and bundles do not cross
|
|
33
|
+
over:
|
|
34
|
+
|
|
35
|
+
- a development build of the app sees only development accounts and bundles;
|
|
36
|
+
- the store app sees only production.
|
|
37
|
+
|
|
38
|
+
So, to test on a phone, ask Condor for three things that match: a **test
|
|
39
|
+
account** in a **test company** in the development environment, a
|
|
40
|
+
**development build** of the app (Android), and to publish your bundle **to that
|
|
41
|
+
test company**. Never iterate on a production company: every publish reaches
|
|
42
|
+
real people.
|
|
43
|
+
|
|
44
|
+
Each iteration on the device is therefore: build → validate → hand over →
|
|
45
|
+
Condor publishes → bring the app to the foreground. That is slow, so do every
|
|
46
|
+
iteration you can in the harness first.
|
|
47
|
+
|
|
48
|
+
## Seeing what the bundle does on the phone
|
|
49
|
+
|
|
50
|
+
**Release builds of the app cannot be inspected.** Ask Condor for a
|
|
51
|
+
development (debuggable) Android build. Then:
|
|
52
|
+
|
|
53
|
+
1. Connect the phone by USB with USB debugging on.
|
|
54
|
+
2. Open `chrome://inspect/#devices` in desktop Chrome and click **inspect** on
|
|
55
|
+
the app's WebView.
|
|
56
|
+
3. In the DevTools console, switch the context selector from `top` to the
|
|
57
|
+
frame `about:srcdoc` — that is your bundle. The `top` frame is the Condor
|
|
58
|
+
shell.
|
|
59
|
+
|
|
60
|
+
There you see your own `console.*` output, uncaught errors, and the shell's
|
|
61
|
+
refusals: a `nack` reaches you only as a `[BridgeClient] command nacked by
|
|
62
|
+
shell` warning, so this is the one place to find out that a command was denied
|
|
63
|
+
or declined.
|
|
64
|
+
|
|
65
|
+
What you will **not** see from the WebView: the native side. A command dropped
|
|
66
|
+
by the app (wrong argument type, unknown command, a request lost while the
|
|
67
|
+
background service starts) leaves no trace in the console — reproduce it in the
|
|
68
|
+
harness instead, whose console says what was dropped and why.
|
|
69
|
+
|
|
70
|
+
## Errors after the fact
|
|
71
|
+
|
|
72
|
+
`BridgeClient.send('jsError', { message, stack })` sends an error to Condor's
|
|
73
|
+
logging and crash reporting (rate limited: 20 per 10 s, 4 KB per field). Those
|
|
74
|
+
are Condor's tools: when you need the errors of a test session, ask your Condor
|
|
75
|
+
contact, with the test account and the time window.
|
|
76
|
+
|
|
77
|
+
## Before you ask for a publish
|
|
78
|
+
|
|
79
|
+
- [ ] `npx condor-mobile-bridge validate -i dist/bundle.zip --strict` exits 0.
|
|
80
|
+
- [ ] Every harness scenario passes (`default`, `cold-start`, `no-devices`,
|
|
81
|
+
`shared-serial`, `offline`).
|
|
82
|
+
- [ ] You know which environment and which company the bundle is for.
|
|
83
|
+
- [ ] You sent `bundleSize` and `bundleHash` with the `.zip`, so the published
|
|
84
|
+
bundle can be checked against what you built.
|
package/docs/pilot-onboarding.md
CHANGED
|
@@ -31,7 +31,7 @@ renders inside the app, talking to the device through the Condor bridge.
|
|
|
31
31
|
```bash
|
|
32
32
|
npm install @condorinst/mobile-bridge
|
|
33
33
|
```
|
|
34
|
-
Pin `"@condorinst/mobile-bridge": "^0.
|
|
34
|
+
Pin `"@condorinst/mobile-bridge": "^0.21.0"` (beta; auto-updates patches).
|
|
35
35
|
|
|
36
36
|
### 2. Start from the example
|
|
37
37
|
Copy [`examples/minimal-home`](../examples/minimal-home) — it already wires the
|
|
@@ -42,15 +42,23 @@ Talk to the app via `BridgeClient`:
|
|
|
42
42
|
```ts
|
|
43
43
|
import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
BridgeClient.
|
|
47
|
-
|
|
45
|
+
BridgeClient.on(BRIDGE_KEYS.userDevice, (d) => render(d)); // listen first
|
|
46
|
+
const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' }); // ack.nativeVersion = capabilities
|
|
47
|
+
const r = await BridgeClient.requestUserDevices(); // resends until the list arrives
|
|
48
|
+
if (r.status === 'timeout') renderEmptyState(); // no reply in 30 s
|
|
48
49
|
BridgeClient.openSettings(); // open native settings
|
|
49
50
|
```
|
|
51
|
+
Never ask for the device list with a single `send('userDevice')`: the first
|
|
52
|
+
request after the app starts is often lost, and an account with no device gets
|
|
53
|
+
no reply. `AGENTS.md` has the full boot sequence.
|
|
50
54
|
|
|
51
55
|
### 4. Test in your browser (no app needed)
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
```bash
|
|
57
|
+
npx condor-mobile-bridge dev -i dist/index.html
|
|
58
|
+
```
|
|
59
|
+
Serves your page inside the same sandbox, shell and policy as the app, with a
|
|
60
|
+
simulated app behind it. Check every scenario in the selector at the top:
|
|
61
|
+
`default`, `cold-start`, `no-devices`, `shared-serial`, `offline`.
|
|
54
62
|
|
|
55
63
|
### 5. Build the bundle
|
|
56
64
|
```bash
|
|
@@ -69,11 +77,15 @@ Errors are outright rejections by the app; warnings install fine and then
|
|
|
69
77
|
misbehave on the device, which is harder to diagnose.
|
|
70
78
|
|
|
71
79
|
### 6. Deliver
|
|
72
|
-
Send `dist/bundle.zip
|
|
73
|
-
|
|
80
|
+
Send `dist/bundle.zip`, with the `bundleSize` and `bundleHash` printed by
|
|
81
|
+
`build`, through the channel Condor gave you. We publish it to your company —
|
|
82
|
+
first to a **development test company**, because a publish reaches every user
|
|
83
|
+
of the company.
|
|
74
84
|
|
|
75
85
|
### 7. Verify on a device
|
|
76
|
-
Condor publishes → the app downloads, validates and renders your Home
|
|
86
|
+
Condor publishes → the app downloads, validates and renders your Home the next
|
|
87
|
+
time it comes to the foreground. [Testing on a device](./device-testing.md)
|
|
88
|
+
explains the environments, how to inspect the WebView, and where errors go.
|
|
77
89
|
|
|
78
90
|
## Hard rules (or the app rejects the bundle)
|
|
79
91
|
|
|
@@ -86,17 +98,19 @@ The CLI handles the inlining/zipping for you — just keep your build single-fil
|
|
|
86
98
|
|
|
87
99
|
## Reference
|
|
88
100
|
|
|
89
|
-
- [Client Guide](./client-guide.md) — full walkthrough (bridge API, key catalog,
|
|
101
|
+
- [Client Guide](./client-guide.md) — full walkthrough (bridge API, key catalog, dev harness)
|
|
102
|
+
- [Testing on a device](./device-testing.md) — from a published bundle to an inspected WebView
|
|
103
|
+
- [Troubleshooting](./troubleshooting.md) — starts from the symptom
|
|
90
104
|
- [`examples/minimal-home`](../examples/minimal-home) — runnable minimal client
|
|
91
105
|
- [README](../README.md) — API reference
|
|
92
106
|
|
|
93
107
|
## Pilot checklist
|
|
94
108
|
|
|
95
109
|
- [ ] SDK installed (`npm i @condorinst/mobile-bridge`)
|
|
96
|
-
- [ ] Example runs in the
|
|
110
|
+
- [ ] Example runs in the harness (`npx condor-mobile-bridge dev`), in every scenario
|
|
97
111
|
- [ ] Home built with `BridgeClient` (handshake + at least one key + one command)
|
|
98
112
|
- [ ] Constraints respected (1 index.html, ≤2 MB, self-contained, single-chunk)
|
|
99
|
-
- [ ] `bundle.zip` generated by the CLI
|
|
113
|
+
- [ ] `bundle.zip` generated by the CLI, and `validate --strict` exits 0
|
|
100
114
|
- [ ] `.zip` delivered to Condor
|
|
101
115
|
- [ ] Home verified on a device
|
|
102
116
|
- [ ] Feedback shared with Condor
|