Builds older than the server's minimum ask to be updated
MergedThis adds a launch and foreground check of /app-version.json. If CFBundleVersion is below ios.minimumBuild, UpdateRequiredView replaces the app tree. The last minimum is cached so a blocked build stays blocked offline.
I found nothing that must change. The gate fails open when there is no cache and a failed or malformed response leaves the cached decision alone. A lowered or missing minimum clears the cache, and the store URL is limited to https or itms-apps. The tests cover each of these and use a namespaced UserDefaults through GrainTestCase, so they don't leak state.
Two things for you to do before the minimum is raised. Both are in your Shortcomings list:
- Confirm in App Store Connect that Xcode Cloud build numbers keep climbing across marketing versions. If they don't, the gate can block current builds.
- Look at the screen on a device. It hasn't been run in the app, and the story comment sheet's separate window hasn't been checked.
Carried forward: the change this pull makes at c5aace157361cdb74ab76243792db3b2865a80ae is the one reviewed at a90e88ce7193c9fbe19fe02f65945473bbcde92f, so that review stands. What is new comes from its base.

What changes
The gate.
AppUpdateGatefetchesAuthManager.serverURL+/app-version.jsonat launch and each time the app becomes active (5 s timeout, overlapping checks skipped), and comparesios.minimumBuildwith the runningCFBundleVersion. Below it,UpdateRequiredViewreplaces the whole view tree: "Update Grain", "This version of Grain is no longer supported. Update to keep using it.", and an Update button openingstoreUrl(the App Store listing if missing or not https/itms-apps). It is styled after the login screen.Offline and failures. The last minimum read is kept in UserDefaults and read at startup, so a blocked build opened offline is blocked at once. A failed request, a non-200, or malformed JSON leaves the cached decision alone; with nothing cached it never blocks. A missing
ioskey orminimumBuildmeans no minimum and clears the cache, which is how lowering the minimum unblocks a build.Why
grain's server is moving off hatk, and the builds installed before the new API will stop working when it does. Today the app has no way to tell anyone to update. This ships first, on the current server, so at cutover the minimum is raised and older builds show this screen instead of errors. The server half is grain.social/grain's "Serve minimum app versions at /app-version.json".
How it was checked
18 XCTests in
GrainTests/AppUpdateGateTests.swift: build vs minimum, missing fields, malformed JSON, a wrong-typedminimumBuild, the cache surviving a failed fetch, a lowered minimum lifting the block. Run once on the iPhone 17 Pro Max simulator: 18 passed. swiftformat and swiftlint clean on the four files. The full suite was not run, and the screen was not looked at in the running app.Shortcomings
CFBundleVersionis not the59inproject.yml: Xcode Cloud replaces it with its own run counter when archiving, and DEVELOPMENT.md says build numbers need only increase within a marketing version. Before raisingminimumBuild, check in App Store Connect that build numbers keep climbing across versions; if a new version could start lower, the gate must compare the marketing version too.🤖 Generated with Claude Code