Skip to content
This repository was archived by the owner on Oct 11, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a41875f
feat(web): run Switchify Remote in the browser over Web Bluetooth
OwenMcGirr Oct 7, 2026
a09fa8f
feat(web): let the web choose another PC and document web support
OwenMcGirr Oct 7, 2026
896c094
fix(web): harden Web Bluetooth lifecycle and web secret storage
OwenMcGirr Oct 7, 2026
d73a22a
fix(web): own device connections per connect attempt and persist keys…
OwenMcGirr Oct 7, 2026
4b37c29
fix(web): explain a Bluetooth picker the browser refuses to open
OwenMcGirr Oct 7, 2026
35058e5
docs(web): describe bounded write cancellation
OwenMcGirr Oct 7, 2026
e9f3a01
fix(web): keep inactive tabs out of the keyboard order
OwenMcGirr Oct 7, 2026
c0cdf5a
fix(web): read tab focus with useSyncExternalStore
OwenMcGirr Oct 7, 2026
ee1376f
fix(web): map picker errors only from requestDevice
OwenMcGirr Oct 7, 2026
8dff52c
fix(web): submit live Enter and isolate stalled probes
OwenMcGirr Oct 8, 2026
de897ad
fix(web): serialize connection characteristic discovery
OwenMcGirr Oct 8, 2026
862ad0f
fix(web): recheck ownership before publishing session
OwenMcGirr Oct 8, 2026
90c14c7
fix(web): retain selected discovery connection for pairing
OwenMcGirr Oct 8, 2026
37ab503
fix(web): address cancellation accessibility and identity review
OwenMcGirr Oct 8, 2026
a48d1d9
chore(deps): update Expo SDK 57 patches
OwenMcGirr Oct 8, 2026
1ad2481
Merge pull request #189 from switchifyapp/codex/expo-patches-188
enaboapps Oct 9, 2026
95aebc8
Merge pull request #187 from switchifyapp/codex/web-review-fixes-186
enaboapps Oct 9, 2026
fdff841
Merge pull request #183 from switchifyapp/codex/web-service-discovery…
enaboapps Oct 9, 2026
e17b2bc
Merge pull request #181 from switchifyapp/codex/web-reliability-180
enaboapps Oct 9, 2026
fedfa2f
fix(web): lock saved-PC changes across tabs and forget failed devices
OwenMcGirr Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Switchify Remote is the accessibility-first Android and iOS companion for controlling Switchify PC over Bluetooth. The development preview provides PC discovery and pairing plus Mouse, Typing, and Window control surfaces.

The app uses Expo SDK 57 with native Bluetooth support. It does **not** run in Expo Go.
The app uses Expo SDK 57 with native Bluetooth support. It does **not** run in Expo Go. It also runs as a [web app](docs/web.md) over Web Bluetooth in Chrome and Edge.

## Requirements

Expand Down
10 changes: 10 additions & 0 deletions docs/accessibility.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Accessibility acceptance criteria

Web selector buttons must include their full current value in the accessible name.
Toggle controls expose pressed/unpressed state; ordinary action buttons must not be
announced as toggles. Busy and disabled controls expose those states to browser
assistive technology. Check the rendered DOM in addition to native prop tests.

On the web, live typing must submit from both keyboard Enter and the visible Enter
control, then restore input focus after success or failure. Failure retains text
and offers Retry Enter. Draft Enter remains multiline. Verify these using actual
browser keyboard events, not only React Native's synthetic submitEditing callback.

Switchify Remote is designed for VoiceOver, TalkBack, iOS Switch Control, and Android Switch Access.

## Custom section layouts
Expand Down
9 changes: 9 additions & 0 deletions docs/physical-smoke-test.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Physical development-preview smoke test

## Web checks

In Chrome on Android against Windows and macOS, verify live keyboard Enter and the
visible Enter control each send once, clear on success, and restore input focus.
After failed delivery, verify text remains and Retry Enter does not duplicate the
text. Draft Enter must insert a newline without sending. Reload the page and
reconnect a saved PC. Check fallback selection after an unreachable remembered PC.
Keep fake-browser checks separate from physical Bluetooth evidence.

Record the app commit, Switchify PC release, phone model/OS, and desktop platform for each run. Never paste pairing credentials or typed personal content into the record.

## Layout edit mode checks
Expand Down
2 changes: 1 addition & 1 deletion docs/protocol-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Switchify Remote is a protocol v1 client. It does not change the Bluetooth servi

Custom section layouts use the separate `switchify.remote.layouts.v2` storage key with `{ version: 2, layouts: { [surface]: { [section]: { columns, cells } } } }`. Fixed section names are unchanged. All existing action IDs and v2 grids remain supported; action IDs are now validated against the shared catalog and surface placement rules instead of their original section. Draft actions are accepted only on Typing. An action may appear once in each section, including sections on different surfaces. Missing, malformed, oversized, unknown-version, duplicate-control, or unknown-action or forbidden-placement section data falls back independently to original section presentation. Version 1 flat layouts are deliberately ignored: their format cannot preserve section boundaries. Existing preference and pairing keys are unchanged, and old builds ignore the v2 key. Hidden capability/mode sections retain saved grids; visible controls continue to use current capability checks and dynamic labels. Records contain no command payloads, text, or authentication material.

The TypeScript compatibility suite carries the canonical authentication and pairing-code vectors from Switchify Android. Android requests the established 517-byte MTU, while both platforms adapt the inner frame payload so the encoded GATT value fits the negotiated ATT limit. Transport tests enforce the 160-byte maximum inner payload, 16 KiB message limit, 10-second partial timeout, duplicate and out-of-order handling, UTF-8 reassembly, response correlation, and sanitized failure behavior.
The TypeScript compatibility suite carries the canonical authentication and pairing-code vectors from Switchify Android. Android requests the established 517-byte MTU, while both platforms adapt the inner frame payload so the encoded GATT value fits the negotiated ATT limit. The [web app](web.md) cannot read the negotiated MTU, so it adapts frames to a fixed 182-byte write. Transport tests enforce the 160-byte maximum inner payload, 16 KiB message limit, 10-second partial timeout, duplicate and out-of-order handling, UTF-8 reassembly, response correlation, and sanitized failure behavior.

The active preview surface supports:

Expand Down
101 changes: 101 additions & 0 deletions docs/web.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Web app

Switchify Remote also runs as a web app built with Expo web. It uses the same screens, protocol client, pairing and layouts as the native apps. Only the platform layer is different.

## Running

```sh
npm run web # development server
npx expo export -p web # static site in dist/
```

Web Bluetooth only works in a secure context, so serve the exported site over HTTPS. `http://localhost` also counts as secure for development.

## Supported browsers

Web Bluetooth is available in Chrome and Edge on Android, Windows, macOS and ChromeOS. Safari, Firefox and every iOS browser lack it. In those browsers the PCs screen shows that Bluetooth is unavailable and names the supported browsers.

## How the web differs

| Area | Native | Web |
|---|---|---|
| Transport | `ReactNativeBleTransport` (react-native-ble-plx) | `WebBluetoothTransport`, selected by `createBleTransport.web.ts` |
| Discovery | Background scan lists every nearby PC | The browser's device picker returns one chosen PC per search. Closing the picker returns to idle (`scan_cancelled`) without showing an error. If the browser refuses to open the picker, because the tap was too long ago or Bluetooth is blocked for the site, the app explains this and asks you to try again. |
| Saved PCs | Rescans and matches the desktop ID | Reuses devices this site may already use (`navigator.bluetooth.getDevices()`), matching by desktop ID. Otherwise it asks once through the picker, which needs a tap. If a remembered device fails to open, for example after a Windows PC changes its Bluetooth address, it is forgotten, so the next attempt looks the PC up again or asks through the picker. |
| Write size | Negotiated MTU (Android requests 517) | Fixed at 182 bytes (a 185-byte ATT MTU). The browser hides the negotiated MTU, and Switchify PC on macOS rejects long (offset) writes. |
| GATT concurrency | Native transaction queue | One GATT operation at a time, because browsers reject overlapping operations |
| Write cancellation | Cancels the native transaction | An in-flight write cannot be aborted, so cancelling waits up to one second for it to finish. That way disconnect cleanup (drag end, modifier release) still reaches the PC. Only a write that never finishes makes writes unavailable until the next connection, matching the native behaviour when cancellation fails. |
| Pairing secrets | Keychain / Keystore via `expo-secure-store` | AES-GCM ciphertext in `localStorage`, encrypted with a non-extractable key held in IndexedDB (`secretStorage.web.ts`). Clearing site data removes the pairing. Saving, removing and tidying saved PCs holds a cross-tab Web Lock (`storageLock.web.ts`), so two open tabs cannot drop each other's PCs. |
| Alerts | Native dialogs | `window.alert` and `window.confirm` (every app alert is a notice or a cancel plus one action) |
| Announcements | `AccessibilityInfo` | A polite ARIA live region (`installPlatform.web.ts`) |
| Diagnostics export | Share sheet | Text file download |
| Inactive tabs | Detached by the navigator | Removed from layout (`tabSceneLayout.web.tsx`), since the web navigator only hides them from screen readers and leaves their controls in the keyboard order |
| Android bridge and Switch Forwarding | Android only | Unavailable |

The protocol, framing limits, authentication and pairing approval are unchanged. See [protocol compatibility](protocol-compatibility.md).

First-time browser device identity creation uses an exclusive Web Lock, so tabs
share one persisted identity. Waiting for a lock is bounded to five seconds. If
locks are unavailable or persistence fails, initialization fails safely rather
than using an unsaved identity. Existing encrypted identities remain readable.
This keeps the existing keys, ciphertext format and native storage path unchanged.

Connection cancellation includes initial cleanup: a stopped or replaced attempt
must not reopen the picker or begin another connection after cleanup completes.

## Typing and probe recovery

Keyboard Enter in live typing sends one Enter, clears only after acknowledgement,
and returns focus to the input. The visible Enter and Retry Enter controls use the
same delivery path. Draft Enter continues to insert a newline without sending.
RN Web 0.21 requires `blurOnSubmit` for this multiline live input; native platforms
continue to use `submitBehavior` without a blur override.

Remembered-device probes have disposable connections and do not occupy the active
session's GATT queue. A stalled probe must time out and disconnect before fallback
selection, and a late probe completion must not affect the chosen PC.

## Hardware validation still needed

Connection discovery now reads status before looking up command and notification
characteristics, one operation at a time. Only PCs advertising `read-v1` are asked
for the read-response characteristic. Discovery shares one ten-second budget and
checks cancellation between operations. Diagnostics identify each lookup using
fixed labels, never browser error text or Bluetooth identifiers.

Issue #182 follows a real Chrome failure after status discovery and before pairing.
The current-code hardware retry narrowed the failure to primary-service discovery
after reconnecting. Sequential characteristic discovery alone did not fix it.
The web transport now explicitly retains the selected completed discovery probe
across scan stop and connection preparation. It reuses that live service and
rereads status before pairing; a changed desktop identity still fails. Normal
scan stop, explicit disconnect, cancellation and failed setup close the probe.
Native transports keep their existing preparation path. A successful real pairing
with this handoff remains required before release.

Automated tests use fake Web Bluetooth devices. Before release, check on real hardware:

- pairing and control from Chrome on Android against Switchify PC on Windows and on macOS
- that 182-byte writes are accepted on each PC platform, and the reconnect behaviour after a PC restarts
- saved-PC reconnection after a browser restart, both with and without `getDevices()` support

## Browser review, 2026-10-08

The issue #180 fixes were checked with computer control against a production Expo
web export served on loopback. An isolated test server substituted a fake
`navigator.bluetooth` before app startup; no PC input or real Bluetooth access was
used. The fixture was outside the shipped app, counted commands without retaining
their text, and was not included in the export.

- Onboarding, discovery, pairing, profile negotiation, and a mouse click completed.
- Keyboard Enter cleared live text and returned focus after one Enter command.
- The visible Enter control also cleared text; fresh typing sent no backspaces.
- A failed Enter retained text and focus; Retry Enter succeeded without resending
chunks or sending backspaces.
- Draft Enter inserted a newline without increasing the command counts.
- Reloading exercised a remembered device whose status read never settled. After
its three-second budget, fallback selected the fake PC and restored controls.
Keyboard Enter continued to work on the new connection.

This is browser UI and fake-transport evidence, not physical Windows/macOS BLE or
screen-reader certification. The hardware checks above remain required.
Loading
Loading