Remote minimum-version gate

A server-controlled floor on the running build. Any build whose versionCode is below the remote minimum runs normally without ads: the Mobile Ads SDK is never initialised, no app-open ad is requested, no banner or interstitial is created. The app itself is never blocked — there is no update wall.

Design decided by Dries, 23 Sept 2026 (Asana 1218577128615630): the gate refuses to initialise the ads SDK below the minimum; it does not refuse to run the app. A build that never requests an ad cannot generate an ad-policy violation, and a mistaken remote value costs “no ads for a session”, never “no app” for a paying customer. In-app updates use FLEXIBLE, not IMMEDIATE. Remote Config failures fail OPEN — ads run as normal. The first version of this branch had a full-screen update wall and an IMMEDIATE update; both were removed on 25 Sept 2026.

What this cannot do: code shipped in the first build that carries the gate cannot run on a device still sitting on an older build. It prevents FUTURE stranding; it is not a remedy for users already on old builds.

Why it exists

AdMob judged five review cycles against a stale 4.1.5 build that was never on Play. This gate makes that impossible to repeat: raise the remote floor and every copy that carries the gate and is below it stops starting the ads SDK on its next launch after the fetch, so such a binary can never again be reviewed with ads live.

The Remote Config key

   
Key min_supported_version_code
Type Number (int) — a versionCode / Android build number
In-app default 0
Meaning The lowest versionCode still allowed to start the ads SDK. A build is supported when currentVersionCode >= min_supported_version_code; below it the app runs without ads.

The value is a versionCode, not a versionName. Read it from docs/RELEASES.md (Play Console: expand the release → Versiecodes), never from a human-facing release name. iOS and Android share this one key; because the same versionName+versionCode has shipped as different code across platforms (see docs/RELEASES.md), set it to the number you actually want to floor.

Fail-open by design

The read fails open. Any failure — offline, fetch timeout, exception, a platform call that never answers, or a key that was never set — resolves to the in-app default 0, which is <= every real build, so ads run as normal. The gate only ever switches ads off when someone deliberately raises the remote value. Its job is to stop KNOWN-BAD versions, not unknown ones.

Mechanically (lib/services/version_gate.dart):

  • RemoteMinVersion.firebase() calls fetchAndActivate() with a minimumFetchInterval of 12 h (the cached value is served in between, so startup is not gated on the network after the first fetch) and an 8 s fetch timeout. RemoteMinVersion.get() adds an overall 10 s timeout and a try/catch → returns 0 on any error or hang.
  • VersionGate is a pure, injectable class (currentVersionCode vs minSupportedVersionCode) with isSupported / isBelowMinimum.
  • The read is not awaited before runApp, so it can never delay the first frame. AdsVersionGate (lib/widgets/ads_version_gate.dart) wraps the app, renders it immediately and unconditionally, and resolves the read after runApp: supported → onAdsAllowed opens the version flag on AdsController; below the minimum → the flag stays closed and the Android flexible update is offered.
  • AdsController has a third flag beside entitlement and consent: _versionAllowsAds, written only via updateVersionGate(). canServeAds is the AND of all three, and every ad surface reads canServeAds, so below the minimum no surface requests an ad and initialize() is a no-op. main() constructs the controller with versionAllowsAds: false; the gate opens it.

Android in-app update

When the build is below the minimum on a Play-installed copy, VersionUpdateFlow (lib/services/version_update_flow.dart) attempts a one-shot FLEXIBLE in_app_update flow: the update downloads in the background while the app stays usable, and is installed (which restarts the app) only when the user next leaves it. IMMEDIATE is not used — it is a blocking full-screen takeover, reserved for a genuine compliance stop. On failure, on non-Android, when no update is available, or when Play does not allow a flexible update, nothing happens: the app keeps running, only ads stay off.

iOS has no in-app update API and gets no prompt. Secondaire’s App Store numeric id, for any future non-blocking update nudge, is 1611881585.

Operating rule — READ THIS BEFORE YOU TOUCH THE VALUE

Raise min_supported_version_code to the previous production versionCode only while an AdMob review is pending, and lower it again once the review clears. Never raise it as a habit.

Every user on a build below the value loses ads (and so stops earning) until they update; the app keeps working for them. A value left high after a review switches ads off on perfectly good builds. Treat raising it as an incident action with an explicit rollback, not a routine knob.

How to set it in the Firebase console

  1. Firebase console → project verbomatique-secondaire → Run → Remote Config.
  2. Add parameter (or edit the existing one):
    • Parameter name: min_supported_version_code
    • Data type: Number
    • Default value: the versionCode you want to floor (e.g. 118). Use 0 to disable the gate entirely.
  3. Publish changes. Clients pick it up on their next fetchAndActivate (within the 12 h minimumFetchInterval; a fresh install fetches on first launch).
  4. To lift the gate after a review, set the value back to 0 and publish.

No platform conditions are needed — one value floors both platforms. If you ever want to floor iOS and Android independently, add a Remote Config condition on platform and give the key two values; the client reads the single resolved value.