Skip to content

feat(updates): require a minimum build and hold the app while an update downloads - #207

Merged
theanmolsharma merged 2 commits into
masterfrom
theanmolsharma/ota-screen-build
Oct 18, 2026
Merged

theanmolsharma merged 2 commits into
masterfrom
theanmolsharma/ota-screen-build

Conversation

@theanmolsharma

Copy link
Copy Markdown
Collaborator

What this adds

Forced upgrade. The update server can now set the oldest native build each platform may run. A build below it shows "Update required" with one button to its store, in place of the app.

  • Set it with node scripts/ota/minimum-build.mjs --channel production --ios N --android M --upload deploy@…. A platform you leave out keeps its live value. The script shows live → new and asks you to type yes before uploading (--yes skips it).
  • It's served at GET /policy as unsigned JSON. Store links are built into the app (constants/links.ts), so the JSON can only carry numbers.
  • The app checks once per launch. It reads the saved answer before the unlock screen mounts, so a blocked launch never shows a Face ID prompt, and the block holds offline.

OTA update screen. When the launch check finds an update, the app is held on "Updating Shroud" until the download is done, then shows Restart. The restart goes through a native reload screen in the theme colours.

  • If the download fails, the user can Try again or Continue on current version. Continue is also offered after 60 s of downloading, so a stalled connection never locks anyone out of their wallet.
  • A roll-back asks for the restart straight away. Replayed old roll-backs are ignored.
  • The bar is indeterminate. Compression stays on, and with it expo-updates can't report byte progress: Android's CompressionInterceptor strips Content-Length before the progress listener sees it. The screen switches to a real percentage by itself if the last file ever reports bytes.

Both screens take the navigator's place rather than covering it. AppLock stays outside on purpose, so a re-lock survives them. Both are off in development and under Detox.

Also in this PR

  • refactor(ui): LockScreen and the update screens share LogoScreenLayout, and SyncScreen uses the new ProgressBar. Both render pixel-identical to before at the default text size, and now scroll at the largest sizes.
  • upload() bug fix: a pointer or policy rewritten within the same second at the same size was skipped by rsync's quick check. This applies to channel pointers too. These small files are now compared with --checksum.

Runtime version

There are no native, app.json or package.json changes. The iOS and Android fingerprints match master (bf3ea9ab…, b8c6efb0…), so this can ship over the air to installed builds. That's also why the new script isn't an npm script: package.json scripts are part of the fingerprint. Only builds that already run this code can be blocked.

Before merging / using it

  • Redeploy update-server (adds /policy). The script refuses to upload until the server serves it.
  • Check on an EAS preview build: publish to preview → Updating → Restart; airplane mode mid-download → Try again / Continue; ota:rollback → Restart; preview minimum above the build → blocked, store opens, still blocked offline, 0 unblocks.
  • Raise a minimum only once the new build is live in its store (100% rollout on Play), and never past an OS version the blocked builds support. RELEASE.md has the rules.
  • The screen can take over after unlock on a slow connection. That trade-off is documented in UpdateGate and RELEASE.md.
  • The policy request goes over clearnet even in Tor-only mode, to the same host as the existing update check. This is documented.

Testing

  • 637 unit tests and 39 update-server tests pass; lint and tsc are clean. The new tests cover the policy module, the OTA state machine, the screens and the gate, the server route, the CLI helpers, and the same-second upload regression (which fails without the fix).
  • The CLI was run end to end against a local server: set both, change one, no-op, refusal without --yes, and a malformed file returning a logged 500.
  • iOS simulator: every state in light and dark mode and at large text sizes, and the status bar with the app's theme forced against the system's.
  • Android emulator: the status bar fix, reproduced without it.
  • Not yet run: a real release build downloading a signed update (see the checklist above).

🤖 Generated with Claude Code

theanmolsharma and others added 2 commits October 16, 2026 20:11
LockScreen's logo, title, subtitle and pinned call to action move into
LogoScreenLayout, so the update screens can use the same layout. Its middle
now scrolls, and the title stops scaling at 1.5x, so nothing is cut off at
the largest text sizes.

SyncScreen's bar becomes ProgressBar, which can also draw an indeterminate
sweep. Both screens render pixel for pixel as before at the default text
size.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…te downloads

A build below the minimum the update server sets for its channel (GET
/policy, set with scripts/ota/minimum-build.mjs) shows "Update required"
with a button to its store, in place of the app. The policy is unsigned,
so the store links are built into the app. The last answer is kept on the
device, so a block also holds offline.

When the launch check finds an over-the-air update, the app is held on an
"Updating" screen until the download is done, then asks for a restart. A
failed download, or one still going after a minute, can be skipped until
the next launch. Both screens are off in development and under Detox.

There are no native, app.json or package.json changes, so the runtime
version is unchanged and this can reach installed builds over the air. The
new script runs with node rather than as an npm script, since package.json
scripts are part of the fingerprint.

Also fixes upload() skipping a pointer rewritten within the same second at
the same size: rsync's quick check took it for unchanged, so pointers and
policies now go with --checksum.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@theanmolsharma
theanmolsharma marked this pull request as ready for review October 18, 2026 01:41
@theanmolsharma
theanmolsharma merged commit 49f66cf into master Oct 18, 2026
4 checks passed
@theanmolsharma
theanmolsharma deleted the theanmolsharma/ota-screen-build branch October 18, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant