Skip to content

feat: in-app update prompts (store + OTA), Settings screen, release tooling - #32

Open
pythonsst wants to merge 14 commits into
mainfrom
feat/restore-app-update-banner
Open

pythonsst wants to merge 14 commits into
mainfrom
feat/restore-app-update-banner

Conversation

@pythonsst

@pythonsst pythonsst commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

In-app update prompts for both kinds of update, Zepto/Blinkit style, shown only when an update exists:

  • New store build (App Store / Google Play): "New version available → Update now"
  • OTA update (expo-updates): downloads silently, then "Update ready → Restart now"
  • Both waiting: store prompt first; after "Later", the OTA prompt follows

Plus a Settings screen, release tooling aligned with the Agastya app, and a few build/OTA fixes found along the way.

Shipped as 1.0.2 (tag v1.0.2; Android versionCode 29, iOS build 52). Branch is now at 1.0.3.

Visual walkthrough of every scenario: https://claude.ai/artifact/9CukaMG2xRRBrK6sqHQpi5


What the user sees

Situation UI
Up to date Nothing. Settings shows only Version 1.0.2 (52).
Store update Bottom sheet "New version available · Update now / Later" (once per app launch) · gold dot on the Home gear · Settings row "Update Available · New ›"
OTA downloaded Bottom sheet "Update ready · Restart now / Later" · gold dot · Settings row "Restart to get the latest improvements"
Both Store sheet first → "Later" → sheet slides down → OTA sheet slides up. Settings row shows the store update.
OTA still downloading Nothing (no action possible yet)
  • iOS "Update now" opens the App Store page. From the Settings row, a native alert asks first: "There is an updated version (x) available on the App Store. Would you like to upgrade?" (Upgrade / Cancel).
  • Android "Update now" opens Google's in-app update screen (Play Core, immediate flow). No extra alert: Google's screen is the confirmation.
  • "Later" hides that sheet until the next cold start. A downloaded OTA also applies automatically on the next cold start.
  • Settings → About shows "Last updated" (OTA publish date) only while an OTA bundle is running.

How it works

Check How
Installed version react-native-device-info → getVersion() (native MARKETING_VERSION / versionName), not package.json
App Store version sp-react-native-in-app-updates → https://itunes.apple.com/lookup?bundleId=<production bundle id>, semver compare
Play version sp-react-native-in-app-updates → Play Core AppUpdateManager (installed vs available versionCode; only works for Play installs)
OTA expo-updates: checkForUpdateAsync() then fetchUpdateAsync(); useUpdates().isUpdatePending drives "ready"

Checks run on mount and when the app returns to the foreground, throttled to once a minute. All state lives in one shared store (useSyncExternalStore), so the sheet, the gear dot and Settings always agree.

Runtime version = app version (app.config.js, synced to Expo.plist / strings.xml by yarn version:bump). An OTA only reaches builds of the version it was published from. Never bump the version for an OTA; bump only for store builds.

Files

Core: port these

File Role
src/hooks/useAppUpdates.ts Shared store + hook: runs checks, exposes storeUpdateAvailable, otaReady, otaDownloading, hasUpdate, latestStoreVersion, applyOta(), goToStore(). Dev-menu previews (Cmd+D → "Preview update prompt: Store / OTA / Store + OTA / Off").
src/hooks/updateState.ts Pure resolvePrompt(): store first, then OTA, respecting "Later".
src/services/storeUpdate.ts Store check + open store. Lazy require of sp-react-native-in-app-updates: react-native-device-info throws at import when its native module is missing, so a top-level import crashes any older binary that receives this JS via OTA.
src/constants/appUpdate.ts App-specific IDs: App Store ID, Android package, iOS production bundle ID (dev/staging builds still look up the production listing).
src/components/AppUpdatePrompt.tsx Bottom sheet (RN Modal). Mounted once at the app root, hidden prop instead of unmounting (keeps "Later"). Slides out before swapping store → OTA.
src/screens/SettingsScreen.tsx "Update Available · New ›" row + alerts; About section.
src/screens/HomeScreen.tsx Gear button with gold dot (hasUpdate).
App.tsx Mounts <AppUpdatePrompt hidden={screen === 'settings'} /> at the root; Settings route; Android back.
__mocks__/expo-updates.js, __mocks__/sp-react-native-in-app-updates.js, jest.config.js Jest mocks
Tests: src/hooks/__tests__/updateState.test.ts, src/components/__tests__/AppUpdatePrompt.test.tsx, src/screens/__tests__/SettingsScreen.test.tsx 20 tests incl. the store → Later → OTA sequence

Release tooling (already in Agastya; ported from there)

scripts/sync-version.js (yarn version:bump x.y.z), scripts/confirm-release-notes.js, scripts/sync-release-notes.js, scripts/eas-update.js, WHATS_NEW.txt, eas.json (changesNotSentForReview, APK profiles, environment).

Repo-specific fixes (probably not needed elsewhere)

  • fb033e6: Expo.plist / AndroidManifest.xml OTA URL pointed at an old EAS project, so eas update never reached builds.
  • d1b891b: Xcode 27: Podfile raises pod deployment targets to 15.1 and builds fmt as C++17.

Porting to another app (e.g. Agastya)

  1. Dependencies: yarn add sp-react-native-in-app-updates (+ react-native-device-info and expo-updates if missing; pin device-info with resolutions if versions conflict), then pod install. This is native code: it only works after a new store build. OTA alone cannot deliver it; the lazy require keeps older binaries from crashing.
  2. Constants (src/constants/appUpdate.ts): App Store numeric ID, Android package, iOS production bundle ID.
  3. Mount the sheet once at the root. With React Navigation: inside NavigationContainer, next to the navigator. Pass hidden while the Settings/Profile screen is focused.
  4. Settings/Profile row: render the "Update Available" row only when storeUpdateAvailable || otaReady.
  5. Theme: replace C, serif, MAX_CONTENT_WIDTH from src/theme.
  6. Jest: copy both mocks + moduleNameMapper entries.
  7. Check runtime version policy: must be app version (Agastya already is: runtimeVersion = version via sync-version.js).
  8. iOS lookup country: no country is passed, so Apple searches the US store. Fine if the app is available in the US; otherwise pass country in checkNeedsUpdate. (Verified: SnapBiodata and Agastya both resolve.)

Test plan

  • Unit tests (20): ordering, "Later", hidden-on-Settings, Settings states, iOS/Android tap paths
  • Simulator: store sheet, OTA sheet, Settings up-to-date / update-row states (dev-menu previews)
  • Real OTA: staging APK + yarn eas:update:staging → "Update ready" → restart → "Last updated" row
  • Android store update: install 1.0.2 from Play, upload 1.0.3 to internal testing → prompt → Google update screen
  • iOS store update: once 1.0.2 is live, install a Release build marked 1.0.1 → prompt → App Store page

🤖 Generated with Claude Code

pythonsst and others added 12 commits November 4, 2026 09:11
Brings back the update banner from 072ffe5, which was dropped when main
was synced to the SnapBiodata app in 1574658.

- Point store lookup at SnapBiodata (App Store 6791839989,
  com.snapbiodata.app)
- Mount the banner under the Home screen header
- Restyle with the app's theme colours
- Add sp-react-native-in-app-updates and Jest mocks

Requires a new native build; not deliverable via OTA alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- No empty gap on Home when the banner is hidden (spacing moves into the
  banner and only renders when visible)
- iOS: open the App Store directly instead of a second confirm alert
- Android: drop the version badge (Play Core reports versionCode, not a
  display version)
- Compare against the installed binary's version, not package.json, which
  an OTA bundle can change
- Keep the check throttle across Home remounts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Podfile post_install: raise pod deployment targets below 15.1 (newer
  Xcode rejects them) and compile the fmt pod as C++17 (fmt 11.0.2 bundled
  with RN 0.81 fails under newer Apple clang with "call to consteval
  function ... is not a constant expression").
- run-ios.sh: "$SIM_NAME…" made bash read the ellipsis as part of the
  variable name and abort with "unbound variable"; brace it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fb12756 moved app.config.js to EAS project ca1367f7 (@agastyaai/snapbiodata)
but Expo.plist and AndroidManifest.xml still pointed at the old project
46cb38f5. `eas update` publishes to the app.config project, so builds
never saw those updates. Native builds made from now on check the right
project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the Home-only banner with a bottom sheet mounted at the app root,
so it shows on every screen (Home, Editor, Preview).

- New store build → "New version available" → App Store page (iOS) or
  Play in-app update (Android)
- OTA downloaded → "Update ready" → restart into the new bundle
- With both pending, the store prompt shows first; "Later" on it slides
  it down and brings up the OTA prompt
- Foreground checks now download a found OTA, so "Update ready" appears
  without waiting for a cold start
- sp-react-native-in-app-updates is required lazily: it pulls in
  react-native-device-info, which throws at import on binaries without
  the native module (the current 1.0.0 production build, same runtime).
  An OTA of this code to that build skips the store check instead of
  crashing on launch
- Dev menu items preview each prompt in debug builds
- Pin react-native-device-info 14.1.1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Port Agastya's scripts/sync-version.js: `yarn version:bump <x.y.z>`
  sets package.json and syncs Android versionName, iOS MARKETING_VERSION
  and the OTA runtime version (strings.xml, Expo.plist); fails loudly if
  a pattern is missing
- app.config.js runtimeVersion now follows package.json, so an OTA only
  reaches binaries of the release it was published from (1.0.1 adds
  native modules the 1.0.0 binary lacks)
- 1.0.1: 1.0.0 is live on the App Store, which won't accept new builds
  for a released version, and the in-app "New version available" prompt
  needs a higher store version to fire

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Gear button in the Home header opens Settings; a gold dot on it shows
  when a store update or a downloaded OTA is waiting
- "App updates" card with four states: up to date (+ "Check for
  updates", last-checked time), new store version (installed → store
  version, App Store "What's new" on iOS, "Update now"), OTA ready
  ("Restart now"), and OTA downloading
- About: installed version + build, and when the running OTA was
  published (an OTA doesn't change the version number)
- Update state moved into one shared store (useSyncExternalStore) so the
  prompt, the gear dot and Settings always agree
- The bottom-sheet prompt is hidden, not unmounted, on Settings, so a
  "Later" is still remembered back on Home
- Dev menu: "Preview update prompt: Off" to leave a preview

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Play production submit: changesNotSentForReview + explicit
  applicationId, as in Agastya. Without changesNotSentForReview the Play
  API tries to send the release for review itself and rejects the upload
  whenever the console needs changes sent manually, so nothing lands on
  the production track
- Staging Android submit: internal track, releaseStatus completed
- Explicit EAS `environment` per profile; staging-apk / production-apk
  profiles for direct installs
- WHATS_NEW.txt flow: production deploy/submit show the notes, ask to
  confirm, then sync them to iOS/Android fastlane metadata
- eas:update:* now prompt for the message and confirm before publishing
- WHATS_NEW.txt for 1.0.2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…here is one

- Settings shows no update UI at all when the app is up to date (only the
  version); the "App updates" card and "Check for updates" are gone
- When a store update or a downloaded OTA exists: one "Update Available ·
  New ›" row at the top
- iOS store update: tapping asks "There is an updated version (x) available
  on the App Store. Would you like to upgrade?" (Upgrade / Cancel); Android
  goes straight to Google's in-app update screen, which is its own confirm
- OTA: "Update Ready" alert with Restart / Later
- About row renamed "Last updated" (when the running OTA was published)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1.0.2 shipped from tag v1.0.2 (Android 29, iOS 52). From here, OTAs
published from this branch target 1.0.3 builds; OTA fixes for 1.0.2
users must be published from v1.0.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The store check compares versions inside sp-react-native-in-app-updates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pythonsst pythonsst changed the title feat: restore in-app update-available banner (OTA + store) feat: in-app update prompts (store + OTA), Settings screen, release tooling Nov 4, 2026
pythonsst and others added 2 commits November 4, 2026 17:16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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