feat(update): tell sideloaded installs that a newer version exists - #179
Merged
Conversation
AGE-110: 64% of 30d-active users sit on v0.4.10 and 0.2% on the newest build, so a client-side fix (the AGE-105 Sentry noise gate) reaches almost nobody. Play hygiene fixes one channel; the direct APK, the self-hosted F-Droid repo and third-party mirrors have no update mechanism at all — a device installed from a downloaded APK has literally no way to learn a newer version exists. - src/lib/update-check-policy.ts: pure, node --test'able decision logic — numeric version compare (a string compare puts 0.4.10 BEFORE 0.4.9, i.e. it would have told the largest stale cohort it was current), a 24h check throttle that survives a backwards clock, per-version dismissal, and a cached last-known-latest so the affordance survives between checks - src/lib/update-check.ts: Android-only runtime wiring. One unauthenticated GET per 24h to the GitHub releases API (every non-Play channel is downstream of a GitHub release; expo-updates cannot replace a native binary, which is what this cohort needs). Never throws. - UpdateBanner on the sessions list: one dismissible strip, no modal. "Not now" sticks for that version only. - Settings "Version" row showed a hard-coded "1.0.0" for every build ever shipped. It now shows the real version, plus "0.4.10 -> 0.4.14" when an update exists (ignoreDismissed: dismissal silences the banner, not the place a user goes to check). - en/zh-Hans strings, catalog parity kept. Tests: 19 new cases in update-check-policy.test.ts; full suite 300 pass, tsc --noEmit clean. Co-Authored-By: Paperclip <noreply@paperclip.ing>
omgoshjosh
pushed a commit
to omgoshjosh/opencode-mobile
that referenced
this pull request
Aug 16, 2026
19 commits behind, three versionNames behind. Notable fixes this pulls in that the evaluation build was missing: 401/403 classified as auth-failed so a wrong password stops the retry loop (dzianisv#170), the Sentry noise gate, the waitlist retry queue, and the in-app "a newer version exists" notice for sideloaded installs (dzianisv#179). Three conflicts, all resolved toward keeping both sides: - android/app/build.gradle: took upstream's versionCode 42 / 0.4.15 but kept the BUILD_VERSION_CODE env override. Evaluation builds need a monotonic code from CI so each side-by-side install supersedes the last; upstream release builds leave it unset and use the literal. - app/(tabs)/index.tsx: the group-by picker and UpdateBanner are unrelated additions to the same region. Kept both, banner above the picker so an update notice reads before the list controls. - app/(tabs)/settings.tsx: upstream and this branch independently fixed the same hard-coded "1.0.0". Took upstream's update-aware row but fed it appVersionLabel rather than CURRENT_VERSION -- sideloaded builds share a versionName across many CI builds, so the version alone cannot identify which APK is actually installed, which is the whole reason that row is being read. 382 tests pass, typecheck clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
AGE-110. Sentry release health, 30d to 2026-08-14:
The gate ships inside the binary, so its ceiling is the share of the base that installs it. Two channels are why that share is 0.2%:
expo-updates, no releases check. A direct-APK install, a self-hosted F-Droid install without auto-update, or a third-party mirror install (APKCombo currently advertises v0.4.10 as its newest listing) cannot learn that a newer version exists. This PR is that half.What changed
src/lib/update-check-policy.ts- pure decision logic, runs undernode --testwith no RN, no network, no clock:0.4.10before0.4.9, i.e. it would tell the single largest stale cohort in the install base that it is up to date. Covered by a test named after exactly that.lastCheckedAtin the future (restored backup, NTP correction) re-enables checking instead of locking it outsrc/lib/update-check.ts- Android-only runtime wiring. One unauthenticated GET to the GitHub releases-latest endpoint per 24h, 8s timeout, drafts/prereleases ignored, never throws. GitHub releases (notexpo-updates) because OTA JS cannot replace a native binary, and every non-Play channel is downstream of a GitHub release.src/components/UpdateBanner.tsx- one dismissible strip above the session list. Not a modal: a launch modal is the fastest way to get uninstalled, and this ships to the whole base. Renders nothing when there is no update, on iOS, or after dismissal."1.0.0"for every build ever shipped. Now shows the real version, and0.4.10 -> 0.4.14with a tap-through when an update exists. It passesignoreDismissed: true- a dismissal silences the interruption, not the place a user goes to check.catalog-parity.test.tsstays green.Privacy
One unauthenticated GET per 24h, no query params, no identifiers, no analytics event. GitHub sees an IP that already downloaded the APK from GitHub. On-device state is a timestamp plus two version strings (AsyncStorage, not SecureStore - nothing here is a credential, and the
persisted-keystripwire covers SecureStore writes, of which this adds none).Verification
node --test src/lib/update-check-policy.test.ts-> 19 passnpm test-> 300 passnpx tsc --noEmit-> cleanHow we will know it worked
node scripts/release-uptake.mjs(#176) is the meter. This lands in the next release, so it can only move the cohort from that version forward - the v0.4.10 users get told once they are on any build that carries this. Play production automation (#177) is what moves the already-installed base.