feat(update): force update when 3+ stable releases behind

UpdateChecker now derives the latest release and a releasesBehind
count from one releases-list call. When the installed version is
forceUpdateThreshold (3) or more stable releases behind, the update
dialog becomes mandatory: barrier and back-press are blocked, the
later/don't-remind actions are hidden, backing out of the installer
returns to the dialog, and the check bypasses the user's
check-for-updates opt-out. Prereleases never count toward the
threshold.
This commit is contained in:
zarzet
2026-07-13 09:44:59 +07:00
parent aae48cc47c
commit 4fc6a6b45d
20 changed files with 555 additions and 240 deletions
+54 -35
View File
@@ -29,6 +29,10 @@ class UpdateInfo {
final DateTime publishedAt;
final bool isPrerelease;
/// How many stable (non-prerelease) releases are newer than the installed
/// version. Drives the forced-update flow.
final int releasesBehind;
const UpdateInfo({
required this.version,
required this.changelog,
@@ -36,63 +40,76 @@ class UpdateInfo {
this.apkDownloadUrl,
required this.publishedAt,
this.isPrerelease = false,
this.releasesBehind = 0,
});
}
class UpdateChecker {
static const String _latestApiUrl =
'https://api.github.com/repos/${AppInfo.githubRepo}/releases/latest';
static const String _allReleasesApiUrl =
'https://api.github.com/repos/${AppInfo.githubRepo}/releases';
/// Installed versions this many stable releases (or more) behind must
/// update before continuing to use the app.
static const int forceUpdateThreshold = 3;
static Future<UpdateInfo?> checkForUpdate({String channel = 'stable'}) async {
if (!Platform.isAndroid) {
return null;
}
try {
final response = await http
.get(
Uri.parse('$_allReleasesApiUrl?per_page=30'),
headers: {'Accept': 'application/vnd.github.v3+json'},
)
.timeout(const Duration(seconds: 10));
if (response.statusCode != 200) {
_log.w('GitHub API returned ${response.statusCode}');
return null;
}
final releases = (jsonDecode(response.body) as List<dynamic>)
.whereType<Map<String, dynamic>>()
.toList();
if (releases.isEmpty) {
_log.i('No releases found');
return null;
}
Map<String, dynamic>? releaseData;
if (channel == 'preview') {
final response = await http
.get(
Uri.parse('$_allReleasesApiUrl?per_page=10'),
headers: {'Accept': 'application/vnd.github.v3+json'},
)
.timeout(const Duration(seconds: 10));
if (response.statusCode != 200) {
_log.w('GitHub API returned ${response.statusCode}');
return null;
}
final releases = jsonDecode(response.body) as List<dynamic>;
if (releases.isEmpty) {
_log.i('No releases found');
return null;
}
releaseData = releases.first as Map<String, dynamic>;
releaseData = releases.first;
} else {
final response = await http
.get(
Uri.parse(_latestApiUrl),
headers: {'Accept': 'application/vnd.github.v3+json'},
)
.timeout(const Duration(seconds: 10));
if (response.statusCode != 200) {
_log.w('GitHub API returned ${response.statusCode}');
return null;
for (final release in releases) {
if (release['prerelease'] != true) {
releaseData = release;
break;
}
}
releaseData = jsonDecode(response.body) as Map<String, dynamic>;
}
if (releaseData == null) {
_log.i('No stable release found');
return null;
}
final tagName = releaseData['tag_name'] as String? ?? '';
final latestVersion = tagName.replaceFirst('v', '');
final isPrerelease = releaseData['prerelease'] as bool? ?? false;
var releasesBehind = 0;
for (final release in releases) {
if (release['prerelease'] == true) continue;
final version = (release['tag_name'] as String? ?? '').replaceFirst(
'v',
'',
);
if (version.isNotEmpty && _isNewerVersion(version, AppInfo.version)) {
releasesBehind++;
}
}
if (!_isNewerVersion(latestVersion, AppInfo.version)) {
_log.i(
'No update available (current: ${AppInfo.version}, latest: $latestVersion, channel: $channel)',
@@ -114,7 +131,8 @@ class UpdateChecker {
final apkUrl = selectedAsset?.url;
_log.i(
'Update available: $latestVersion (prerelease: $isPrerelease), '
'Update available: $latestVersion (prerelease: $isPrerelease, '
'releases behind: $releasesBehind), '
'APK asset: ${selectedAsset?.name ?? 'none'}, APK URL: $apkUrl',
);
@@ -125,6 +143,7 @@ class UpdateChecker {
apkDownloadUrl: apkUrl,
publishedAt: publishedAt,
isPrerelease: isPrerelease,
releasesBehind: releasesBehind,
);
} catch (e) {
_log.e('Error checking for updates: $e');