@condorinst/mobile-bridge 0.22.0 → 0.23.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 +133 -34
- package/CHANGELOG.md +175 -0
- package/README.md +30 -481
- package/contract/bridge-contract.json +32 -21
- package/dist/cli.cjs +126 -29
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +126 -29
- package/dist/cli.js.map +1 -1
- package/dist/dev-harness.global.js +388 -55
- package/dist/dev.cjs +389 -55
- package/dist/dev.cjs.map +1 -1
- package/dist/dev.d.cts +239 -9
- package/dist/dev.d.ts +239 -9
- package/dist/dev.js +388 -55
- package/dist/dev.js.map +1 -1
- package/dist/index.cjs +30 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -26
- package/dist/index.d.ts +154 -26
- package/dist/index.js +29 -8
- package/dist/index.js.map +1 -1
- package/docs/agent-guide.md +9 -265
- package/docs/bridge-contract.md +313 -26
- package/docs/client-guide.md +34 -400
- package/docs/device-testing.md +234 -0
- package/docs/pilot-onboarding.md +41 -115
- package/docs/shell-integration.md +292 -0
- package/docs/troubleshooting.md +19 -11
- package/examples/minimal-home/README.md +2 -2
- package/examples/minimal-home/package.json +1 -1
- package/examples/minimal-home/src/main.ts +23 -3
- package/llms-full.txt +764 -865
- package/llms.txt +13 -15
- package/package.json +1 -1
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Shell-side integration — for Condor's app shell
|
|
2
|
+
|
|
3
|
+
**Client bundles never use anything on this page.** A bundle talks to the app
|
|
4
|
+
only through `BridgeClient` — see [`AGENTS.md`](../AGENTS.md). This page is for
|
|
5
|
+
the Condor shell that hosts the bundle (`BridgeShell`), the legacy `Bridge`
|
|
6
|
+
singleton that talks to the native layer directly, and their mock mode and dev
|
|
7
|
+
panel. It is kept out of `llms-full.txt` on purpose.
|
|
8
|
+
|
|
9
|
+
## Quickstart (legacy, shell-side `Bridge`)
|
|
10
|
+
|
|
11
|
+
> **Not for client bundles.** This is the API the Condor shell uses to talk to
|
|
12
|
+
> the native layer. A client bundle cannot reach it from the sandbox — use
|
|
13
|
+
> `BridgeClient` (above, and in the next section).
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
17
|
+
|
|
18
|
+
// Versioned handshake with the native side (can be called at any time;
|
|
19
|
+
// recommended on the client's DOMContentLoaded)
|
|
20
|
+
const ack = await Bridge.handshake({ clientVersion: '1.0.0' });
|
|
21
|
+
console.log('Native bridge:', ack.nativeVersion, 'accepted:', ack.accepted);
|
|
22
|
+
|
|
23
|
+
// Subscribe to an observable key coming from the background
|
|
24
|
+
const unsubscribe = Bridge.on(BRIDGE_KEYS.deviceConnection, (payload) => {
|
|
25
|
+
console.log('Device:', payload);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Command: enable real-time for one device
|
|
29
|
+
Bridge.send('realTime', { serialNo: 1234, value: true });
|
|
30
|
+
|
|
31
|
+
// Native hook: open the app's native Settings screen
|
|
32
|
+
Bridge.openSettings();
|
|
33
|
+
|
|
34
|
+
// Cleanup
|
|
35
|
+
unsubscribe();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> The recommended model for white-label clients is **`BridgeClient`** (sandbox
|
|
39
|
+
> iframe), not the legacy `Bridge`. See below.
|
|
40
|
+
|
|
41
|
+
## Sandbox iframe — `BridgeClient` + `BridgeShell`
|
|
42
|
+
|
|
43
|
+
The recommended model isolates the external client code inside an
|
|
44
|
+
`<iframe sandbox="allow-scripts">`, communicating over `window.postMessage`
|
|
45
|
+
with schema-validated messages and a default-deny policy.
|
|
46
|
+
|
|
47
|
+
### For the external client — `BridgeClient`
|
|
48
|
+
|
|
49
|
+
API **identical** to the legacy `Bridge`, but over a postMessage transport.
|
|
50
|
+
The client imports it and never notices it is sandboxed:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
54
|
+
|
|
55
|
+
const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
|
|
56
|
+
console.log('Shell:', ack.shellVersion, 'accepted:', ack.accepted);
|
|
57
|
+
|
|
58
|
+
const unsubscribe = BridgeClient.on(BRIDGE_KEYS.userDevice, (payload) => {
|
|
59
|
+
console.log('Devices:', payload);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
BridgeClient.send('realTime', { serialNo: 1234, value: true });
|
|
63
|
+
BridgeClient.openSettings();
|
|
64
|
+
|
|
65
|
+
// System back button: report whether you can go back, and react to the OS back.
|
|
66
|
+
BridgeClient.reportNavigation(currentView !== 'home'); // call on every route change
|
|
67
|
+
BridgeClient.onBack(() => goToPreviousPage()); // fired only when you reported true
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The `reportNavigation` / `onBack` pair lets your page own the Android back
|
|
71
|
+
button — walking your in-page navigation before the app leaves the Home. Opt-in;
|
|
72
|
+
skip it and the app just stays on Home when back is pressed. See
|
|
73
|
+
[docs/bridge-contract.md](./bridge-contract.md).
|
|
74
|
+
|
|
75
|
+
**Differences vs `Bridge`:**
|
|
76
|
+
- Does not expose `mockEmit`, `installDevPanel`, `isMock`, `getMockState` —
|
|
77
|
+
dev/mock APIs live only on the shell.
|
|
78
|
+
- `handshake` resolves with `{ shellVersion, nativeVersion, accepted }`. Since
|
|
79
|
+
0.17.0 the shell relays `nativeVersion` into the iframe — it is the only
|
|
80
|
+
capability signal you get, and it is `null` when the shell could not reach
|
|
81
|
+
the native side and against shells older than 0.6.0. `accepted` is always `true`; never branch on it.
|
|
82
|
+
- Commands not allowed by the `ShellPolicy` are silently dropped (with a
|
|
83
|
+
`console.warn`). The external client has no visibility into which commands
|
|
84
|
+
do or do not exist.
|
|
85
|
+
|
|
86
|
+
### For the Condor-controlled shell — `BridgeShell`
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { BridgeShell, BRIDGE_KEYS, type ShellPolicy } from '@condorinst/mobile-bridge';
|
|
90
|
+
|
|
91
|
+
const iframe = document.getElementById('client-frame') as HTMLIFrameElement;
|
|
92
|
+
|
|
93
|
+
const policy: ShellPolicy = {
|
|
94
|
+
readOnlyKeys: [
|
|
95
|
+
BRIDGE_KEYS.userDevice,
|
|
96
|
+
BRIDGE_KEYS.deviceConnection,
|
|
97
|
+
BRIDGE_KEYS.deviceLightSensors,
|
|
98
|
+
BRIDGE_KEYS.realTime,
|
|
99
|
+
],
|
|
100
|
+
allowedCommands: ['userDevice', 'realTime', 'openSettings'],
|
|
101
|
+
requiresConfirmation: ['syncTime', 'deleteMemory'],
|
|
102
|
+
deniedCommands: ['dfu'],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const shell = new BridgeShell({
|
|
106
|
+
iframe,
|
|
107
|
+
policy,
|
|
108
|
+
onConfirmationRequest: async ({ command, args }) => {
|
|
109
|
+
return await showConfirmationModal(command, args); // shell UI
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Layer 1 — bootstrap with an override prefix.
|
|
114
|
+
//
|
|
115
|
+
// Use bootstrapClientIframe(clientPath) when HTML comes over HTTP:
|
|
116
|
+
await shell.bootstrapClientIframe('https://api.condor.example/whitelabel/bundle.html');
|
|
117
|
+
//
|
|
118
|
+
// Use bootstrapClientIframeWithHtml(htmlString) when the HTML is already in
|
|
119
|
+
// memory — required for an asset bundled at file:// (Android WebView cannot
|
|
120
|
+
// fetch over file://), a build-time inlined bundle, etc:
|
|
121
|
+
declare const __CLIENT_HTML__: string; // e.g. via esbuild.define
|
|
122
|
+
await shell.bootstrapClientIframeWithHtml(__CLIENT_HTML__);
|
|
123
|
+
|
|
124
|
+
// Layer 2 — iframe attestation.
|
|
125
|
+
const verify = await shell.verifySandbox();
|
|
126
|
+
if (!verify.ok) {
|
|
127
|
+
// Abort the boot — Layer 1 failed.
|
|
128
|
+
throw new Error('Sandbox compromised: bridges still reachable from iframe.');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// From here on, BridgeShell routes postMessage ↔ native Bridge
|
|
132
|
+
// automatically. Nothing else to do at boot.
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Layered defense (propagation of the native channel into the iframe)
|
|
136
|
+
|
|
137
|
+
| Layer | Where | What it does |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| **1** | `BridgeShell.bootstrapClientIframe` | `srcdoc` with a prefix that deletes `window.CondorNative`, `window.flutter_inappwebview` and `window.CondorBridge` before any client script runs. Defense in depth only: the WebView injects the channel into every frame, so a child iframe created by the client gets it back (and on iOS `window.webkit.messageHandlers` cannot be locked). |
|
|
140
|
+
| **2** | `BridgeShell.verifySandbox` + `BridgeClient` handler | Round-trip `postMessage` to attest the iframe state (`nativeChannelReachable` since 0.18.0). |
|
|
141
|
+
| **3** | (Native side) | Shell nonce, stamped on every outgoing message via `Bridge.setOutgoingStamp`, validated natively. **This is the guarantee.** |
|
|
142
|
+
| **4** | (Fallback if 1+2 fail) | Dual `InAppWebView` — shell and client in separate WebViews. |
|
|
143
|
+
|
|
144
|
+
## Host detection
|
|
145
|
+
|
|
146
|
+
The package detects automatically:
|
|
147
|
+
|
|
148
|
+
- **Android / iOS** — `window.CondorNative`, a JavaScript channel registered with
|
|
149
|
+
`webview_flutter` (`addJavaScriptChannel('CondorNative')`). Messages are sent as a
|
|
150
|
+
JSON string. Takes precedence when present.
|
|
151
|
+
- **Android / iOS (legacy)** — `window.flutter_inappwebview` (via the `flutter_inappwebview`
|
|
152
|
+
package). Still supported for the transition; will be removed in a future minor.
|
|
153
|
+
- **Browser outside the app** (dev/test): **mock mode** activates automatically
|
|
154
|
+
for `Bridge`. A client bundle (`BridgeClient`) runs in the browser only
|
|
155
|
+
through the `dev` harness.
|
|
156
|
+
|
|
157
|
+
## Mock mode (shell-side `Bridge`)
|
|
158
|
+
|
|
159
|
+
> For code that uses the legacy `Bridge` directly — the shell, tests,
|
|
160
|
+
> Storybook. A client bundle uses `BridgeClient`, which has no mock of its own:
|
|
161
|
+
> develop it with `condor-mobile-bridge dev` (above).
|
|
162
|
+
|
|
163
|
+
When the package runs outside the Flutter WebView (regular browser, Vite dev
|
|
164
|
+
server, Storybook), the adapter activates mock mode automatically. You can
|
|
165
|
+
develop in the browser without building the APK.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
169
|
+
|
|
170
|
+
if (Bridge.isMock) {
|
|
171
|
+
// Subscribe to a listener as usual
|
|
172
|
+
Bridge.on(BRIDGE_KEYS.deviceConnection, (payload) => {
|
|
173
|
+
console.log('Device:', payload);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Simulate the native side sending an event
|
|
177
|
+
Bridge.mockEmit(BRIDGE_KEYS.deviceConnection, {
|
|
178
|
+
serialNo: 1234,
|
|
179
|
+
connected: true,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Handshake in mock is EXPLICIT
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// In mock, handshake does NOT resolve by itself — the client/test responds.
|
|
188
|
+
const ackPromise = Bridge.handshake({ clientVersion: '1.0.0' });
|
|
189
|
+
|
|
190
|
+
if (Bridge.isMock) {
|
|
191
|
+
Bridge.mockEmit('handshake-ack', {
|
|
192
|
+
nativeVersion: '0.1.0',
|
|
193
|
+
accepted: true,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const ack = await ackPromise;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Design decision: it forces tests to be intentional about the native state. In
|
|
201
|
+
real mode (inside the Flutter WebView), the ack arrives naturally from native.
|
|
202
|
+
|
|
203
|
+
### `Bridge.send` in mock
|
|
204
|
+
|
|
205
|
+
Messages sent via `Bridge.send` (or helpers like `openSettings`) are recorded
|
|
206
|
+
in an internal FIFO buffer (cap 100) + `console.debug`. Inspect via
|
|
207
|
+
`Bridge.getMockState()`:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
Bridge.send('realTime', { enabled: true });
|
|
211
|
+
|
|
212
|
+
const state = Bridge.getMockState();
|
|
213
|
+
console.log(state.messages);
|
|
214
|
+
// → [{ handlerName: 'CondorBridge', payload: {...}, timestamp: ... }]
|
|
215
|
+
console.log(state.subscriberKeys);
|
|
216
|
+
// → ['deviceConnection']
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Dev panel (shell-side `Bridge`)
|
|
220
|
+
|
|
221
|
+
> The `dev` harness installs this panel for you. Call `installDevPanel` yourself
|
|
222
|
+
> only when hosting `Bridge` directly.
|
|
223
|
+
|
|
224
|
+
An opt-in HTML panel to inspect the bridge in the browser during development.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { Bridge } from '@condorinst/mobile-bridge';
|
|
228
|
+
|
|
229
|
+
if (Bridge.isMock) {
|
|
230
|
+
Bridge.installDevPanel({
|
|
231
|
+
position: 'bottom-right', // 'bottom-right' | 'bottom-left' | 'top-*'
|
|
232
|
+
maxMessages: 20, // last N shown
|
|
233
|
+
theme: 'dark', // 'dark' | 'light'
|
|
234
|
+
startCollapsed: false,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Contents:
|
|
240
|
+
|
|
241
|
+
- Header with a "MOCK" badge, sent counter, minimize button.
|
|
242
|
+
- Sent log — last N messages (timestamp, type, truncated payload).
|
|
243
|
+
- Trigger emit — dropdown of `BRIDGE_KEYS` + JSON textarea + "Emit" button
|
|
244
|
+
(fires `Bridge.mockEmit`) + "Clear sent" button.
|
|
245
|
+
|
|
246
|
+
Idempotent — calling `installDevPanel` again replaces the previous panel.
|
|
247
|
+
Throws if called in real mode (`!Bridge.isMock`).
|
|
248
|
+
|
|
249
|
+
### Auto-respond toggle
|
|
250
|
+
|
|
251
|
+
To close the full loop in the browser (client clicks a button → native
|
|
252
|
+
responds → subscriber fires), pass a **policy** that maps `send` to `emit`.
|
|
253
|
+
The package provides the **mechanism** (checkbox UI + hook); you provide the
|
|
254
|
+
**policy** (app-specific mappings).
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { Bridge, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
258
|
+
|
|
259
|
+
if (Bridge.isMock) {
|
|
260
|
+
Bridge.installDevPanel({
|
|
261
|
+
startAutoRespondEnabled: true, // default false
|
|
262
|
+
autoRespondDelayMs: 50, // default 50 (simulates IPC)
|
|
263
|
+
onSendForAutoRespond(msg) {
|
|
264
|
+
const payload = msg.payload as { type?: string };
|
|
265
|
+
switch (payload?.type) {
|
|
266
|
+
case 'handshake':
|
|
267
|
+
return {
|
|
268
|
+
key: 'handshake-ack',
|
|
269
|
+
payload: { nativeVersion: '0.1.0 (mock)', accepted: true },
|
|
270
|
+
};
|
|
271
|
+
case 'ping':
|
|
272
|
+
return {
|
|
273
|
+
key: BRIDGE_KEYS.pingPong,
|
|
274
|
+
payload: { data: 'pong' },
|
|
275
|
+
};
|
|
276
|
+
default:
|
|
277
|
+
return null; // do not auto-respond to this send
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Behavior:
|
|
285
|
+
|
|
286
|
+
- The toggle appears in the panel **only when** `onSendForAutoRespond` is
|
|
287
|
+
passed — without a policy, the panel keeps the manual-emit UX.
|
|
288
|
+
- The toggle state persists in memory for the panel's lifetime. Re-install
|
|
289
|
+
resets to `startAutoRespondEnabled`.
|
|
290
|
+
- Errors thrown by the policy or the internal `triggerEmit` are caught and
|
|
291
|
+
logged (`console.error`) — they do not destabilize the bridge.
|
|
292
|
+
- Use `BRIDGE_KEYS` to avoid string-magic in the mappings.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
## "The device list never came" (stuck on loading)
|
|
4
4
|
|
|
5
5
|
The handshake worked, `systemStatus` arrived, and `userDevice` never answered.
|
|
6
|
-
Two different causes produce exactly this symptom, and the bundle cannot tell
|
|
7
|
-
them apart.
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
**From `nativeVersion` 0.18.0 the app fixes both causes below itself**: it
|
|
8
|
+
re-sends a request lost while its background service starts, and it answers a
|
|
9
|
+
failed lookup with empty arrays and `error` (`NO_DEVICES`, `NETWORK`, `SESSION`,
|
|
10
|
+
`UNKNOWN`). `requestUserDevices()` turns those into `no-devices` and `error`.
|
|
11
|
+
Below 0.18.0 the two causes produce exactly the same silence, and the bundle
|
|
12
|
+
cannot tell them apart.
|
|
13
|
+
|
|
14
|
+
### Cause 1 (below 0.18.0): the request was sent before the app's background service was up
|
|
10
15
|
|
|
11
16
|
`userDevice` and `ping` are not answered by the page that hosts you: the app
|
|
12
17
|
forwards them to a background service that reads the devices. On a cold start
|
|
@@ -15,12 +20,12 @@ request sent at boot is often **lost without a reply**. Nothing re-sends the
|
|
|
15
20
|
list when the service comes up. Meanwhile the handshake, `systemStatus` and
|
|
16
21
|
`language` work from the start, which makes the bridge look ready.
|
|
17
22
|
|
|
18
|
-
### Cause 2: the account has no linked device
|
|
23
|
+
### Cause 2 (below 0.18.0): the account has no linked device
|
|
19
24
|
|
|
20
25
|
The app replies to `userDevice` only when the list loads. When the server
|
|
21
26
|
answers that the account has no device — or the request fails for any other
|
|
22
|
-
reason (no internet, expired session) — nothing is sent to the bundle.
|
|
23
|
-
`userDevice` with empty arrays
|
|
27
|
+
reason (no internet, expired session) — nothing is sent to the bundle. An app
|
|
28
|
+
below 0.18.0 never sends a `userDevice` with empty arrays.
|
|
24
29
|
|
|
25
30
|
### What to do
|
|
26
31
|
|
|
@@ -29,16 +34,19 @@ Use the helper, which handles both:
|
|
|
29
34
|
```ts
|
|
30
35
|
const result = await BridgeClient.requestUserDevices(); // resends with backoff for 30 s
|
|
31
36
|
if (result.status === 'ok') render(result.devices);
|
|
32
|
-
else
|
|
37
|
+
else if (result.status === 'no-devices') renderEmpty('none linked'); // confirmed (0.18.0+)
|
|
38
|
+
else renderEmpty('could not load'); // error (0.18.0+) or timeout — always with "Try again"
|
|
33
39
|
```
|
|
34
40
|
|
|
35
41
|
- It resends at 1.5 s, 3 s, 6 s and then every 10 s, and on every
|
|
36
42
|
`systemStatus` while it waits. `BridgeClient.waitForService()` does the same
|
|
37
43
|
with `ping` if you only need to know the service is up.
|
|
38
44
|
- **`systemStatus.hasDevices` is a hint, not a verdict.** It is `true` only
|
|
39
|
-
after the app loaded a non-empty list from the server.
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
after the app loaded a non-empty list from the server. From 0.18.0,
|
|
46
|
+
`systemStatus.hasDevicesKnown` says whether it is the server's answer; below
|
|
47
|
+
it, `false` means either "no device" or "the app could not ask". Word the
|
|
48
|
+
empty state so it is true in both cases, and always offer a retry with
|
|
49
|
+
`getFromServer: true`.
|
|
42
50
|
- Keep the `userDevice` subscription for the life of the page: roster changes
|
|
43
51
|
arrive on it without a request.
|
|
44
52
|
|
|
@@ -164,7 +172,7 @@ First question: **which browser setup?**
|
|
|
164
172
|
run every scenario, not just `default`. `cold-start` and `no-devices` are the
|
|
165
173
|
ones that catch real bugs.
|
|
166
174
|
|
|
167
|
-
In the harness `nativeVersion` is `0.
|
|
175
|
+
In the harness `nativeVersion` is `0.18.0` by default. Run it with
|
|
168
176
|
`--native-version null` to reproduce a shell that could not reach the native
|
|
169
177
|
side; keep a branch that renders in that case too.
|
|
170
178
|
|
|
@@ -6,8 +6,8 @@ handshakes with the native side, listens for connection/real-time events, and
|
|
|
6
6
|
sends commands back — all through `BridgeClient`, with the boot sequence that
|
|
7
7
|
survives a cold start and an account with no device.
|
|
8
8
|
|
|
9
|
-
Use it as a starting skeleton for your own bundle.
|
|
10
|
-
[
|
|
9
|
+
Use it as a starting skeleton for your own bundle. The procedure is in
|
|
10
|
+
[`AGENTS.md`](../../AGENTS.md).
|
|
11
11
|
|
|
12
12
|
## Run it
|
|
13
13
|
|
|
@@ -32,7 +32,7 @@ interface DeviceRow {
|
|
|
32
32
|
realTimeOn: boolean;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
type Phase = 'loading' | 'ready' | 'empty' | 'outside-shell';
|
|
35
|
+
type Phase = 'loading' | 'ready' | 'empty' | 'error' | 'outside-shell';
|
|
36
36
|
|
|
37
37
|
const state = {
|
|
38
38
|
phase: 'loading' as Phase,
|
|
@@ -74,6 +74,19 @@ function telemetryFor(
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function applyDevices(payload: UserDevicePayload): void {
|
|
77
|
+
// From nativeVersion 0.18.0 a failed lookup also arrives here, with `error`
|
|
78
|
+
// and empty arrays. It is not an empty roster: keep what is on screen.
|
|
79
|
+
if (payload.error !== undefined) {
|
|
80
|
+
if (payload.error === 'NO_DEVICES') {
|
|
81
|
+
state.devices = new Map();
|
|
82
|
+
state.probablyNoDevices = true;
|
|
83
|
+
state.phase = 'empty';
|
|
84
|
+
} else if (state.devices.size === 0) {
|
|
85
|
+
state.phase = 'error';
|
|
86
|
+
}
|
|
87
|
+
render();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
77
90
|
const next = new Map<number, DeviceRow>();
|
|
78
91
|
for (const cfg of payload.devicesConfig) {
|
|
79
92
|
const telemetry = telemetryFor(cfg, payload);
|
|
@@ -140,11 +153,14 @@ function body(): string {
|
|
|
140
153
|
case 'loading':
|
|
141
154
|
return '<p class="empty">Loading your devices…</p>';
|
|
142
155
|
case 'empty':
|
|
156
|
+
// probablyNoDevices: confirmed (0.18.0+) or suggested by an older app.
|
|
143
157
|
return `<p class="empty">${
|
|
144
158
|
state.probablyNoDevices
|
|
145
159
|
? 'No device is linked to your account yet.'
|
|
146
160
|
: 'Could not load your devices.'
|
|
147
161
|
}</p><p class="empty"><button onclick="__home.retry()">Try again</button></p>`;
|
|
162
|
+
case 'error':
|
|
163
|
+
return '<p class="empty">Could not load your devices.</p><p class="empty"><button onclick="__home.retry()">Try again</button></p>';
|
|
148
164
|
case 'ready':
|
|
149
165
|
return [...state.devices.values()].map(renderCard).join('');
|
|
150
166
|
}
|
|
@@ -192,9 +208,13 @@ BridgeClient.on(BRIDGE_KEYS.realTime, (payload) => {
|
|
|
192
208
|
|
|
193
209
|
async function loadDevices(getFromServer: boolean): Promise<void> {
|
|
194
210
|
const result = await BridgeClient.requestUserDevices({ getFromServer });
|
|
195
|
-
|
|
211
|
+
// ok, no-devices and error also reached applyDevices through the
|
|
212
|
+
// subscription, which already rendered them.
|
|
213
|
+
if (result.status !== 'timeout') return;
|
|
214
|
+
// An older app gives only a hint; with devices on screen, keep them.
|
|
215
|
+
if (state.devices.size > 0) return;
|
|
196
216
|
state.probablyNoDevices = result.hasDevices === false;
|
|
197
|
-
state.phase = 'empty';
|
|
217
|
+
state.phase = state.probablyNoDevices ? 'empty' : 'error';
|
|
198
218
|
render();
|
|
199
219
|
}
|
|
200
220
|
|