No need update check for web

This commit is contained in:
Marc Rejohn Castillano 2026-03-12 20:38:19 +08:00
parent 0bd2a94ece
commit 9267ebee2c
5 changed files with 83 additions and 31 deletions

View File

@ -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 publiclyaccessible URL pointing at an APK stored in Supabase Storage.
final String downloadUrl;
@ -25,8 +26,8 @@ class AppVersion {
factory AppVersion.fromMap(Map<String, dynamic> 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;

View File

@ -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<AppUpdateScreen> {
Future<void> _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<String, dynamic>) {
_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<String, dynamic>? bestRow;
if (rowList != null) {
for (final r in rowList) {
if (r is Map<String, dynamic>) {
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<String, dynamic>) {
_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<AppUpdateScreen> {
} 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({

View File

@ -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 rethrown; the caller can
/// decide whether to ignore them or surface them to the user.
Future<AppUpdateInfo> 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,

View File

@ -1438,7 +1438,7 @@ packages:
source: hosted
version: "2.1.0"
pub_semver:
dependency: transitive
dependency: "direct main"
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"

View File

@ -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: