Skip to main content
Version: 57.2.0

Upgrade Banner

Two-tier system for notifying users about app updates: a soft warning (dismissible toast) and a hard block (full-screen, no dismiss). Admin-configurable thresholds per platform.

How It Works

  1. Backend exposes GET /version-check?version=N&platform=web|mobile (public, no auth)
  2. Admin configures version thresholds via the admin panel
  3. Client calls useUpgradeCheck() on mount, which compares the app's build number against the thresholds
  4. Result: ok (no action), warning (persistent toast), or required (blocking screen)

Setup

1. Backend: Register the VersionCheckPlugin

import {TerrenoApp, VersionCheckPlugin} from "@terreno/api";

const app = new TerrenoApp({userModel: User})
.register(new VersionCheckPlugin())
.start();

This adds the public GET /version-check endpoint. The admin routes (GET/PUT /admin/version-config) are automatically available if you're using AdminApp.

2. Frontend: Add build number injection

Create or update app.config.ts in your Expo app root. Use the resolveBuildNumber helper from @terreno/rtk/buildNumber:

import {resolveBuildNumber} from "@terreno/rtk/buildNumber";
import type {ConfigContext, ExpoConfig} from "expo/config";

export default ({config}: ConfigContext): ExpoConfig => ({
...config,
extra: {
...config.extra,
buildNumber: resolveBuildNumber({configValue: config.extra?.buildNumber}),
},
} as ExpoConfig);

resolveBuildNumber resolves the build number in this order:

  1. override option (set to a number for local testing)
  2. configValue option (e.g. from app.json expo.extra.buildNumber or EAS Build)
  3. EXPO_PUBLIC_BUILD_NUMBER env var (customizable via envVar option)
  4. git rev-list --count HEAD (default)
  5. undefined if git is unavailable (version check will be skipped)

This sets Constants.expoConfig.extra.buildNumber when Metro loads the config. By default it tracks the git commit count.

If you change buildNumber and nothing updates: restart the dev server (bun expo start or expo start --clear). expo-constants is filled when the bundle is built; hot reload does not always re-read app.config.ts.

3. Frontend: Wire up the hook

In your root layout:

import {useUpgradeCheck} from "@terreno/rtk";
import {UpgradeRequiredScreen} from "@terreno/ui";

function RootLayoutNav() {
const {isRequired, requiredMessage, onUpdate} = useUpgradeCheck();

if (isRequired) {
return (
<UpgradeRequiredScreen
message={requiredMessage ?? "Please update to continue."}
onUpdate={onUpdate}
/>
);
}

return <Stack>{/* normal routes */}</Stack>;
}

The hook handles:

  • ok: Nothing happens, app runs normally
  • warning: Shows a persistent toast with the admin-configured message
  • required: Returns isRequired: true so you can render the blocking screen
  • No build number: Skips the check entirely (dev environment)

4. Admin: Configure thresholds

Navigate to the admin panel and select "Version Config". Set:

  • Web Warning Version: Build number below which web users see a warning toast
  • Web Required Version: Build number below which web users are blocked
  • Mobile Warning Version: Same for mobile
  • Mobile Required Version: Same for mobile
  • Warning Message: Text shown in the toast
  • Required Message: Text shown on the blocking screen
  • Update URL (optional): App store or download link for mobile updates

All version fields default to 0, meaning no constraint until configured.

CI/CD: Build Number in GitHub Actions

EAS Build

In your eas.json, use a pre-build hook or set the build number via environment:

# .github/workflows/build.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for git rev-list --count

- name: Install dependencies
run: bun install

- name: Build with EAS
run: eas build --platform all --non-interactive

Since app.config.ts runs git rev-list --count HEAD at build time, the build number is automatically set. The key requirement is fetch-depth: 0 in the checkout step to ensure the full git history is available.

Web Deployments

For static web builds (e.g. expo export):

jobs:
deploy-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install dependencies
run: bun install

- name: Export web build
run: npx expo export --platform web

- name: Deploy
run: # your deployment step

The app.config.ts runs during expo export, so the build number is embedded in the static output.

Verifying the Build Number

After building, you can verify the build number is set correctly:

# Check the current commit count locally
git rev-list --count HEAD

This number should match what Constants.expoConfig.extra.buildNumber returns in the running app.

Architecture

App mounts → useUpgradeCheck() fires
→ GET /version-check?version=N&platform=web|mobile
→ "ok" → normal app
→ "warning" → normal app + persistent toast
→ "required" → UpgradeRequiredScreen (blocks everything)

Update behavior when user taps "Update":

  • Web: window.location.reload() — picks up the latest deployment
  • Mobile: Opens updateUrl via Linking.openURL() (e.g. App Store link)