fix(waitlist): queue + retry failed signups instead of silently opening mailto (#165)

A signup that hit a network error, the 8s timeout or a 5xx was handed straight
to a `mailto:` composer. That path is lossy by design: it only works if the user
actually presses send, and if we keep reconciling the support inbox into Brevo
list 4 forever (AGE-61's hourly job). 20 of 21 signups were lost that way before
that reconciler existed, and Play's active base is ~100% on v0.4.10+ — so this
was current builds leaking, not just the ~436 stale sideloads.

Now:
- Failed-but-retryable signups are persisted on-device
  (`opencode.waitlist.pending.v1`, AsyncStorage) and retried on every app
  foreground (`app/_layout.tsx`) and on the Add Connection screen mount.
- 4xx stays non-retryable: the server will never accept that address, so we ask
  the user to fix it instead of queueing garbage forever.
- `mailto:` is now only ever opened by an explicit user tap ("Still not working?
  Email us instead"), shown after 3 failed attempts, or offered in an alert when
  device storage itself refuses the write — never as the silent default.
- The UI tells the truth: "Saved on this device — we'll finish signing you up as
  soon as you're back online" instead of implying it was sent.
- `WaitlistResult.fallback` -> `retryable`, `shouldFallbackToMailto` ->
  `isRetryableFailure`: the decision is about retry, not about mail.

Queue policy: dedupe by email, cap 5 entries, 30-day TTL, corrupt/foreign JSON
is discarded rather than replayed. Storage and the clock are injected so the
whole thing runs under `node --test` (16 new tests, incl. the acceptance case:
offline signup -> queued -> reconnect -> reaches the server, no mail client).

Also commits the AGE-61 measurement artifacts that were only ever local
(`distribution/waitlist-signup-path-coverage.md`, `scripts/play-version-share.mjs`)
and updates the doc's "current builds still leak" section, which this fixes.

Refs AGE-87, AGE-61.

Co-authored-by: engineer <engineer@macbookpro.lan>
This commit is contained in:
Den
2026-08-14 01:30:23 -07:00
committed by GitHub
parent 98233d351f
commit 2f81d34200
9 changed files with 698 additions and 57 deletions

View File

@@ -29,10 +29,14 @@ returns 0 results) and the app is not on IzzyOnDroid, so those channels contribu
the owner manually re-downloads. The hourly reconciler
(`VibeBrowserProductPage/.github/workflows/waitlist-mailto-reconcile.yml`) is what keeps those
signups from being lost, and it is not a temporary measure.
3. **Stale builds are not the only source of mailto signups.** In current builds the fallback also
fires on network error, timeout (8s) and 5xx (`src/lib/waitlist.ts:shouldFallbackToMailto`).
A user on v0.4.12 with flaky mobile data takes the same lossy path. Any plan that assumes
"ship an update and the leak closes" is wrong.
3. **Stale builds are not the only source of mailto signups** — they are, as of AGE-87, the only
*remaining* one. Until v0.4.12 the fallback also fired on network error, timeout (8s) and 5xx,
so a user on a current build with flaky mobile data took the same lossy path. That path is gone:
a failed signup is now persisted on-device (`opencode.waitlist.pending.v1`, AsyncStorage) and
retried on every app foreground (`src/lib/waitlist.ts` queue section, flushed from
`app/_layout.tsx`). `mailto:` is only ever opened by an explicit user tap after repeated
retry failures. Expect the reconciler's `synced_count` to trend toward the sideload cohort only.
Any plan that assumes "ship an update and the leak closes" is still wrong for those ~436 devices.
## Method / reproducing