From 9267ebee2c95ca5dbe81d73dcfadf83f32cec597 Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Thu, 12 Mar 2026 20:38:19 +0800 Subject: [PATCH] No need update check for web --- lib/models/app_version.dart | 18 +++++---- lib/screens/admin/app_update_screen.dart | 48 ++++++++++++++++++------ lib/services/app_update_service.dart | 45 ++++++++++++++++------ pubspec.lock | 2 +- pubspec.yaml | 1 + 5 files changed, 83 insertions(+), 31 deletions(-) diff --git a/lib/models/app_version.dart b/lib/models/app_version.dart index 86d8c31b..d3804377 100644 --- a/lib/models/app_version.dart +++ b/lib/models/app_version.dart @@ -2,13 +2,14 @@ /// `AppUpdateService` fetches the most recent entry and compares it against the /// running application's build number. class AppVersion { - /// Incrementing integer that matches the Android `versionCode`. - final int versionCode; + /// Version string, e.g. `1.2.3` or `2026.03.12`. Stored as text in + /// the database so semantic versions are allowed. + final String versionCode; /// If the device is running a build number that is *strictly less* than this /// value the update is considered "forced" and the user cannot continue - /// using the existing install. - final int minVersionRequired; + /// using the existing install. Also a string. + final String minVersionRequired; /// A publicly‑accessible URL pointing at an APK stored in Supabase Storage. final String downloadUrl; @@ -25,8 +26,8 @@ class AppVersion { factory AppVersion.fromMap(Map map) { return AppVersion( - versionCode: map['version_code'] as int, - minVersionRequired: map['min_version_required'] as int, + versionCode: map['version_code']?.toString() ?? '', + minVersionRequired: map['min_version_required']?.toString() ?? '', downloadUrl: map['download_url'] as String, releaseNotes: map['release_notes'] as String? ?? '', ); @@ -40,8 +41,9 @@ class AppUpdateInfo { /// table was empty (should not happen in normal operation). final AppVersion? latestVersion; - /// Current build number as reported by ``package_info_plus``. - final int currentBuildNumber; + /// Current build number / version string. May be empty on non-Android + /// platforms since checkForUpdate is skipped there. + final String currentBuildNumber; /// ``true`` when ``currentBuildNumber < latestVersion.versionCode``. final bool isUpdateAvailable; diff --git a/lib/screens/admin/app_update_screen.dart b/lib/screens/admin/app_update_screen.dart index d4e02183..996c3b65 100644 --- a/lib/screens/admin/app_update_screen.dart +++ b/lib/screens/admin/app_update_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -41,16 +42,40 @@ class _AppUpdateScreenState extends ConsumerState { Future _loadCurrent() async { try { final client = Supabase.instance.client; - final data = await client - .from('app_versions') - .select() - .order('version_code', ascending: false) - .limit(1) - .maybeSingle(); - if (data is Map) { - _versionController.text = data['version_code']?.toString() ?? ''; - _minController.text = data['min_version_required']?.toString() ?? ''; - _notesController.text = data['release_notes'] ?? ''; + final rows = await client.from('app_versions').select().maybeSingle(); + // when using text versions we can't rely on server-side ordering; instead + // parse locally and choose the greatest semantic version. + if (rows is List) { + final rowList = rows as List?; + Version? best; + Map? bestRow; + if (rowList != null) { + for (final r in rowList) { + if (r is Map) { + final v = r['version_code']?.toString() ?? ''; + Version parsed; + try { + parsed = Version.parse(v); + } catch (_) { + continue; + } + if (best == null || parsed > best) { + best = parsed; + bestRow = r; + } + } + } + } + if (bestRow != null) { + _versionController.text = bestRow['version_code']?.toString() ?? ''; + _minController.text = + bestRow['min_version_required']?.toString() ?? ''; + _notesController.text = bestRow['release_notes'] ?? ''; + } + } else if (rows is Map) { + _versionController.text = rows['version_code']?.toString() ?? ''; + _minController.text = rows['min_version_required']?.toString() ?? ''; + _notesController.text = rows['release_notes'] ?? ''; } } catch (_) {} } @@ -169,10 +194,11 @@ class _AppUpdateScreenState extends ConsumerState { } else { url = ''; } - if (url.isEmpty) + if (url.isEmpty) { throw Exception( 'could not obtain public url, check bucket CORS and policies', ); + } _logs.add('Public URL: $url'); // upsert new version in a single statement await client.from('app_versions').upsert({ diff --git a/lib/services/app_update_service.dart b/lib/services/app_update_service.dart index 271e9281..3b557f2e 100644 --- a/lib/services/app_update_service.dart +++ b/lib/services/app_update_service.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:ota_update/ota_update.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -48,26 +49,48 @@ class AppUpdateService { /// (network down, no supabase instance) are simply re‑thrown; the caller can /// decide whether to ignore them or surface them to the user. Future checkForUpdate() async { - final pkg = await PackageInfo.fromPlatform(); - final currentBuild = int.tryParse(pkg.buildNumber) ?? 0; - - final serverVersion = await _fetchLatestVersion(); - - if (serverVersion == null) { - // table empty – nothing to do + // only run on Android devices; web and other platforms skip + if (!Platform.isAndroid) { return AppUpdateInfo( - currentBuildNumber: currentBuild, + currentBuildNumber: '', latestVersion: null, isUpdateAvailable: false, isForceUpdate: false, ); } - final available = currentBuild < serverVersion.versionCode; - final force = currentBuild < serverVersion.minVersionRequired; + final pkg = await PackageInfo.fromPlatform(); + final currentVersion = pkg.buildNumber.trim(); + + final serverVersion = await _fetchLatestVersion(); + + if (serverVersion == null) { + return AppUpdateInfo( + currentBuildNumber: currentVersion, + latestVersion: null, + isUpdateAvailable: false, + isForceUpdate: false, + ); + } + + // compare using semantic versioning where possible + Version safeParse(String s) { + try { + return Version.parse(s); + } catch (_) { + return Version(0, 0, 0); + } + } + + final vCurrent = safeParse(currentVersion); + final vLatest = safeParse(serverVersion.versionCode); + final vMin = safeParse(serverVersion.minVersionRequired); + + final available = vCurrent < vLatest; + final force = vCurrent < vMin; return AppUpdateInfo( - currentBuildNumber: currentBuild, + currentBuildNumber: currentVersion, latestVersion: serverVersion, isUpdateAvailable: available, isForceUpdate: force, diff --git a/pubspec.lock b/pubspec.lock index 4660a5a6..491fae61 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1438,7 +1438,7 @@ packages: source: hosted version: "2.1.0" pub_semver: - dependency: transitive + dependency: "direct main" description: name: pub_semver sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" diff --git a/pubspec.yaml b/pubspec.yaml index 80287827..f7a2e08f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: flutter_image_compress: ^2.4.0 dio: ^5.1.2 package_info_plus: ^9.0.0 + pub_semver: ^2.1.1 ota_update: ^7.1.0 dev_dependencies: