#498·PairDrop

[Bug] Stuck offline after app switch on iPadOS PWA — reconnect on visibilitychange is dead code (window.hiddenProperty is always truthy)

Author: tomeksonCreated Jul 7, 2026Updated Jul 13, 2026
Labelsbug

Describe the bug On iPadOS, when PairDrop is installed as a home screen web app, switching to another app and back leaves PairDrop permanently "offline": no devices are discovered anymore and the only way to recover is to force-quit the web app and open it again.

I looked into the client code and I believe the root cause is that the reconnect-on-visibilitychange path — the one designed exactly for this scenario — is dead code:

https://github.com/schlagmichdoch/PairDrop/blob/1b0c9c9d903c3cfc706df1303dc75c3b54d04c77/public/scripts/network.js#L295-L298

javascript
_onVisibilityChange() {
    if (window.hiddenProperty) return;
    this._connect();
}

window.hiddenProperty is not the page's visibility state — it is the name of the property (the string 'hidden'), as defined in the polyfill:

https://github.com/schlagmichdoch/PairDrop/blob/1b0c9c9d903c3cfc706df1303dc75c3b54d04c77/public/scripts/util.js#L43-L49

A non-empty string is always truthy, so _onVisibilityChange() always returns early and _connect() is never called. The check should be:

javascript
if (document[window.hiddenProperty]) return;

For comparison, the original Snapdrop code this was ported from was if (document.hidden) return; (snapdrop/network.js#L78-L81), so this looks like a regression introduced with the polyfill refactor.

Why this only bites iPadOS PWAs

The bug affects all platforms, but elsewhere another recovery path masks it:

  • Desktop browsers don't tear down the WebSocket when the tab/window loses focus, so no reconnect is needed.
  • On iPhone, iOS usually jettisons the suspended page and reloads it on return → fresh page, fresh connection.
  • On iPadOS (more RAM), the frozen page is kept in memory and resumed. The WebSocket was severed during suspension, but WebKit often does not fire close on it after resume (readyState stays OPEN — a "zombie" socket), and in standalone home screen web apps navigator.onLine/the online event are unreliable. That leaves visibilitychange as the only recovery path — and it never fires _connect() due to the bug above.

Note that even with the one-line fix, _connect() may still bail on a zombie socket because _isConnected() sees readyState === OPEN (network.js#L70-L72), and the client has no ping timeout of its own to detect a dead connection (it only answers server pings). So a robust fix for the iPadOS case is probably: on visibilitychange → visible, close/discard the existing socket and reconnect (or verify liveness with a ping/pong round-trip before trusting it).

To Reproduce Steps to reproduce the behavior:

  1. On an iPad, add https://pairdrop.net/ to the home screen and open it from there
  2. Confirm other devices on the same network are visible
  3. Switch to any other app (full screen) for a while, e.g. 1–2 minutes
  4. Switch back to PairDrop
  5. PairDrop believes it is offline; no devices are shown; it never recovers until the web app is force-quit and reopened

Expected behavior On returning to the app, PairDrop reconnects to the signaling server and rediscovers devices, as it effectively does on iPhone (via page reload) and on desktop (socket survives).

Screenshots

Smartphone (please complete the following information):

  • Device: iPad Air (5th generation)
  • OS: iPadOS 26.5.2
  • Browser: Safari (home screen web app / PWA)
  • Version: latest

Bug occurs on official PairDrop instance https://pairdrop.net/ Yes Version: v1.11.2 (code verified identical on the live instance)

Bug occurs on self-hosted PairDrop instance Not tested (code path is the same)

Additional context Possibly related: #260 (automatic reconnect on error). Devices that work fine: Windows PC, MacBook, iPhone (iPhone occasionally needs a manual refresh, consistent with the page-reload recovery path described above).