version_gate

version_gate demo

A lightweight Flutter package that checks for app updates on the App Store and Google Play Store with zero configuration.

pub package build status pub points popularity likes License: MIT Platform


Table of Contents


Why version_gate?

Two popular packages exist for version checking — new_version (broken since 2022, no Dart 3 support) and upgrader (overly complex, heavy dependencies, hard to customize UI). version_gate was built to be:

  • Zero-config — reads bundle ID and app version automatically
  • Lightweight — only 4 dependencies, all Flutter Favorites
  • Customizable — use built-in widgets or bring your own UI
  • Modern — built for Dart 3 and Flutter 3.x

Features

  • Zero configuration — no API keys, no manual bundle IDs
  • Smart frequency guard — checks once daily (configurable)
  • Three update modes — optional, flexible, forced
  • Minimum version enforcement — auto-force updates below a threshold
  • Built-in widgets — dialog, banner, full-screen blocker
  • Localization — 10 languages out of the box (Arabic, Spanish, French, German, Turkish, Urdu, Chinese, Japanese, Korean)
  • Custom HTTP headers — works behind corporate proxies and CDNs
  • CallbacksonUpdateAvailable, onNoUpdate, onError for analytics
  • Custom UI friendly — get the data, build your own UI
  • Release notes — shows "What's new" from the store
  • Fail-safe — never crashes your app, even on network errors
  • Minimal footprint — 4 lightweight, production-grade dependencies

Screenshots

Update Dialog

Update dialog with version info, release notes, and action buttons


Installation

1. Add dependency

dependencies:
  version_gate: ^1.1.0

2. Install packages

flutter pub get

3. Import

import 'package:version_gate/version_gate.dart';

4. Platform Setup

The url_launcher dependency requires a small platform config so your app can open store URLs.

Android — add to android/app/src/main/AndroidManifest.xml (inside <manifest>, before <application>):

<queries>
  <intent>
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="https" />
  </intent>
</queries>

iOS — add to ios/Runner/Info.plist (inside <dict>):

<key>LSApplicationQueriesSchemes</key>
<array>
  <string>https</string>
</array>

Note: If you skip this step, openStore() may silently fail on Android 11+ and iOS.


Quick Start

The simplest possible usage — 3 lines, zero config:

final result = await VersionGate().check();

if (result != null && result.hasUpdate) {
  result.showBuiltInDialog(context);
}

That's it. The package automatically reads your app's bundle ID and version, checks the store, and shows a Material dialog if an update exists.


Usage Patterns

Pattern A: Custom UI

Maximum flexibility — handle the result yourself.

final result = await VersionGate().check();

if (result != null && result.hasUpdate) {
  showDialog(
    context: context,
    builder: (_) => AlertDialog(
      title: Text('Update Available'),
      content: Text('Version ${result.storeVersion} is ready.'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: Text('Later'),
        ),
        ElevatedButton(
          onPressed: () => result.openStore(),
          child: Text('Update Now'),
        ),
      ],
    ),
  );
}

Pattern B: Built-in Dialog

Drop-in dialog — customize text only.

final result = await VersionGate().check();

if (result != null && result.hasUpdate) {
  result.showBuiltInDialog(
    context,
    title: 'New Version Available',
    message: 'Version ${result.storeVersion} is now available.',
    updateButtonText: 'Update Now',
    laterButtonText: 'Later',
    showReleaseNotes: true,
  );
}

Pattern C: Forced Update

Block the entire app until the user updates.

final result = await VersionGate(
  updateMode: UpdateMode.forced,
).check();

if (result != null && result.hasUpdate) {
  result.showBuiltInDialog(context);
  // Shows full-screen blocker — cannot be dismissed
}

Pattern D: Banner

Persistent banner while the user continues using the app.

final result = await VersionGate(
  updateMode: UpdateMode.flexible,
).check();

if (result != null && result.hasUpdate) {
  // Add to your widget tree:
  UpdateBanner(
    result: result,
    onDismiss: () => setState(() => showBanner = false),
  );
}

Pattern E: Weekly Check

final result = await VersionGate(
  checkFrequency: CheckFrequency.oncePerWeek,
).check();

Pattern F: Override Package ID

Only needed when the store bundle ID differs from the app's local ID (e.g., white-label apps).

final result = await VersionGate(
  packageIdOverride: 'com.production.appid',
).check();

Pattern G: Minimum Version

Force updates for critically outdated versions while keeping optional mode for everyone else.

final result = await VersionGate(
  updateMode: UpdateMode.optional,
  minimumVersion: '2.0.0', // anything below 2.0.0 becomes forced
).check();

if (result != null && result.hasUpdate) {
  result.showBuiltInDialog(context);
  // Users below 2.0.0 get a full-screen blocker
  // Users above 2.0.0 get a dismissible dialog
}

Pattern H: Custom Headers

For apps behind corporate proxies or CDNs that require auth headers.

final result = await VersionGate(
  headers: {
    'Authorization': 'Bearer your-token',
    'X-Custom-Header': 'value',
  },
).check();

Pattern I: Callbacks

Hook into the check lifecycle for analytics tracking.

final result = await VersionGate(
  onUpdateAvailable: (result) {
    analytics.track('update_available', {
      'store_version': result.storeVersion,
      'local_version': result.localVersion,
    });
  },
  onNoUpdate: (result) {
    analytics.track('app_up_to_date');
  },
  onError: (error, result) {
    crashlytics.recordError(error);
  },
).check();

Pattern J: Localization

Show built-in widgets in a different language.

// Use a built-in language
final result = await VersionGate(
  strings: UpdateStrings.arabic(),
).check();

// Or customize individual strings
final result = await VersionGate(
  strings: UpdateStrings(
    title: 'Nueva versión',
    updateButtonText: 'Actualizar',
    laterButtonText: 'Después',
  ),
).check();

Available languages: English (default), Arabic, Spanish, French, German, Turkish, Urdu, Chinese (Simplified), Japanese, Korean.


Configuration

All parameters are optional:

VersionGate(
  checkFrequency: CheckFrequency.onceDaily,    // How often to check
  updateMode: UpdateMode.optional,              // Dialog behavior
  minimumVersion: '2.0.0',                     // Force below this version
  headers: {'Authorization': 'Bearer token'},  // Custom HTTP headers
  strings: UpdateStrings.arabic(),             // Localized widget text
  onUpdateAvailable: (result) { ... },         // Update found callback
  onNoUpdate: (result) { ... },                // Up to date callback
  onError: (error, result) { ... },            // Error callback
  packageIdOverride: 'com.production.appid',   // Override bundle ID
  countryCode: 'us',                           // iTunes region
)

Check Frequencies

Value Behavior
CheckFrequency.onceDaily Check once every 24 hours (default)
CheckFrequency.oncePerSession Check on every cold app launch
CheckFrequency.oncePerWeek Check once every 7 days
CheckFrequency.always Always check — dev/testing only
CheckFrequency.custom(hours: 12) Check every N hours

Update Modes

Value Behavior
UpdateMode.optional Dismissible dialog — user can tap "Later" (default)
UpdateMode.flexible Persistent banner — app still usable
UpdateMode.forced Full-screen blocker — cannot dismiss

API Reference

VersionGate

The main entry point. Create an instance and call check().

final gate = VersionGate(
  checkFrequency: CheckFrequency.onceDaily,
  updateMode: UpdateMode.optional,
  packageIdOverride: null,
  countryCode: 'us',
);

final result = await gate.check(); // Returns VersionCheckResult?

check() returns null when the frequency guard skips the check (e.g., already checked today).

VersionCheckResult

Property Type Description
hasUpdate bool true if store version > local version
localVersion String Installed version (e.g., "1.2.0")
storeVersion String Store version (e.g., "1.5.0")
releaseNotes String? "What's new" from the store
storeUrl String Direct link to store listing
updateMode UpdateMode The mode set in VersionGate() (may be overridden to forced by minimumVersion)
lastChecked DateTime Timestamp of this check
error String? Error message if check failed
strings UpdateStrings? Localized text for built-in widgets
Method Description
showBuiltInDialog(context, {...}) Shows the built-in update UI (dialog or block screen)
openStore() Opens the store listing in the platform browser

How It Works

App launches
    │
    ▼
Was a check already done today? ──YES──▶ Skip. Return null.
    │
   NO
    │
    ▼
Read local version + package name (automatic via package_info_plus)
    │
    ▼
Detect platform
    │
    ├── iOS ──▶ iTunes Lookup API (JSON) ── free, no auth
    └── Android ──▶ Google Play Store page (HTML parse)
    │
    ▼
Save timestamp to SharedPreferences (only after successful API call)
    │
    ▼
Compare versions (semantic versioning)
    │
    ├── Same ──▶ VersionCheckResult(hasUpdate: false)
    └── Store is newer ──▶ VersionCheckResult(hasUpdate: true)

Key design decisions:

  • Timestamp is saved after a successful API response, not after showing a dialog. If the network fails, it retries on next launch.
  • The version comparator handles all semver formats: 1.2.3, 1.2.3+45, 1.2, 2.0.0-beta.1.
  • All errors are caught internally. check() never throws — it returns hasUpdate: false with an error field on failure.

Comparison with Other Packages

Feature version_gate upgrader new_version
Zero config (auto bundle ID) ❌ Manual ❌ Manual
Dart 3 / Flutter 3.x ❌ Broken ⚠️ Issues
Once-daily check
Custom check frequency
Forced update mode
Minimum version enforcement
Localization (10 languages)
Custom HTTP headers
Analytics callbacks
Custom UI (bring your own) ✅ Easy ❌ Hard
Built-in dismissible dialog
Release notes from store
Lightweight dependencies ✅ (4 pkgs) ❌ Heavy ✅ Minimal
Active maintenance (2026) ❌ Stale ❌ Stale

Troubleshooting

Q: check() always returns null. The frequency guard is skipping the check. You already checked within the configured period. Use CheckFrequency.always for testing, or call FrequencyGuard.reset() to clear the stored timestamp.

Q: hasUpdate is always false on Android. Google may have changed the Play Store HTML structure. File an issue — the parser may need a new regex pattern.

Q: The app on the store uses a different bundle ID. Pass packageIdOverride: 'com.your.store.id' to VersionGate().

Q: I need to check a specific App Store region. Pass countryCode: 'sa' (or any valid iTunes country code) to VersionGate().


Contributing

Contributions are welcome! Here's how:

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Please make sure to update tests as appropriate and run dart pub publish --dry-run before submitting.

Reporting Bugs

Found a bug? Open an issue with:

  • Flutter version (flutter --version)
  • Platform (iOS/Android)
  • Steps to reproduce
  • Expected vs actual behavior

Feature Requests

Have an idea? Open a feature request.


License

MIT License — Copyright (c) 2026 aboelnasr.com

See LICENSE for details.


Made with ❤️ by aboelnasr.com

Buy Me a Coffee

Libraries

version_gate
A lightweight Flutter package that checks for app updates on the App Store and Google Play Store with zero configuration.