-
Notifications
You must be signed in to change notification settings - Fork 2
Feat/lib update tips live versions #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,203 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import 'dart:async'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import 'dart:convert'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import 'package:http/http.dart' as http; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import '../utils/sdk_version.dart'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import 'app_version_provider.dart'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Latest published release of the DevConnect Manage Tool desktop app | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// itself (not the SDKs the user-facing tip covers). Fetched on | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// construction from GitHub Releases, then auto-refreshed every | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// [_ttl]. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// The currently-running version is sourced from [appVersionProvider] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// (which reads `PackageInfo.fromPlatform()` — the binary's own | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Info.plist, set from `pubspec.yaml` at build time). No Dart-side | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// version constants to drift. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class AppRelease { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final String version; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final String htmlUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final String? notes; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const AppRelease({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required this.version, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required this.htmlUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.notes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class AppReleaseState { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Version of the running app, sourced from [appVersionProvider]. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Null until the platform channel has responded. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final String? currentVersion; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final AppRelease? release; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final DateTime? fetchedAt; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final String? error; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const AppReleaseState({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.currentVersion, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.release, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.fetchedAt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.error, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static const empty = AppReleaseState(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// True when we have both the running version and a published | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// release, and the running version is older. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool get hasUpdate { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final r = release; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final cur = currentVersion; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (r == null || cur == null) return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return compareSdkVersions(cur, r.version) < 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Unoauthenticated GitHub REST endpoint for the most recent release. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 60 req/hr limit per IP — desktop app caches, so a single cold | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// start is one call. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const _releasesUrl = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'https://api.github.com/repos/ridelinktechs/devconnect-manage-kit/releases/latest'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final appUpdateProvider = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| StateNotifierProvider<AppUpdateNotifier, AppReleaseState>((ref) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final notifier = AppUpdateNotifier(ref); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref.onDispose(notifier._onDispose); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return notifier; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class AppUpdateNotifier extends StateNotifier<AppReleaseState> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static const _ttl = Duration(minutes: 30); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static const _timeout = Duration(seconds: 5); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final Ref _ref; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Timer? _refresh; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AppUpdateNotifier(this._ref) : super(AppReleaseState.empty) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _refresh = Timer.periodic(_ttl, (_) => _refreshNow()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ignore: discarded_futures | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _bootstrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Manually re-fetch. UI surfaces this as a "Retry" affordance. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Future<void> refresh() => _refreshNow(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// One-shot init: load the running version, then kick off the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// GitHub fetch. Order matters — `hasUpdate` needs both pieces. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Future<void> _bootstrap() async { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // PackageInfo.fromPlatform() can throw on rare platform-channel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // failures. Without this try/catch the exception bubbles out of | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the StateNotifier constructor (which ignores it) AND leaves the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GitHub fetch never started. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String? cur; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cur = await _ref.read(appVersionProvider.future); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!mounted) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state = AppReleaseState(error: 'package_info: $e'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!mounted) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state = AppReleaseState(currentVersion: cur); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await _refreshNow(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Future<void> _refreshNow() async { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Make sure we have the running version before checking | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // `hasUpdate`. Subsequent fetches after the first will already | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // have it cached in state. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String? cur; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cur = state.currentVersion ?? await _ref.read(appVersionProvider.future); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!mounted) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state = AppReleaseState( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| release: state.release, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentVersion: state.currentVersion, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchedAt: DateTime.now(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'package_info: $e', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!mounted) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final resp = await http | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+108
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .get( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Uri.parse(_releasesUrl), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: const { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Accept': 'application/vnd.github+json', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .timeout(_timeout); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!mounted) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (resp.statusCode != 200) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Preserve the last-known release so a transient 5xx / 403 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // doesn't suddenly flip the UI from "Update available" back | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // to a stale "loading" state. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state = AppReleaseState( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentVersion: cur, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| release: state.release, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchedAt: DateTime.now(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'HTTP ${resp.statusCode}', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final json = jsonDecode(resp.body); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (json is! Map<String, dynamic>) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state = AppReleaseState( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentVersion: cur, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| release: state.release, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchedAt: DateTime.now(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'unexpected payload', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final tag = json['tag_name']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final url = json['html_url']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (tag is! String || url is! String) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state = AppReleaseState( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentVersion: cur, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| release: state.release, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchedAt: DateTime.now(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'missing tag_name / html_url', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Strip leading "v" if the tag uses it (e.g. "v1.0.4" -> "1.0.4"). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final version = tag.startsWith('v') ? tag.substring(1) : tag; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state = AppReleaseState( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentVersion: cur, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| release: AppRelease( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| version: version, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| htmlUrl: url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| notes: json['body'] is String ? json['body'] as String : null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchedAt: DateTime.now(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } on TimeoutException { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!mounted) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state = AppReleaseState( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentVersion: cur, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| release: state.release, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchedAt: DateTime.now(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'timeout', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!mounted) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state = AppReleaseState( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentVersion: cur, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| release: state.release, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchedAt: DateTime.now(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: e.toString(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void _onDispose() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _refresh?.cancel(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||
| import 'package:package_info_plus/package_info_plus.dart'; | ||
|
|
||
| /// Single source of truth for the desktop app's own version. | ||
| /// | ||
| /// Reads from `PackageInfo.fromPlatform()` — that returns the version | ||
| /// baked into the compiled binary's Info.plist (macOS), which Flutter | ||
| /// sets from `pubspec.yaml` during `flutter build`. So bumping | ||
| /// `pubspec.yaml` is the only thing required to "ship" a new version; | ||
| /// no Dart constants to update. | ||
| /// | ||
| /// Returns `AsyncValue<String>` so callers can render a loading state | ||
| /// while the platform channel roundtrip is in flight. Resolved value | ||
| /// is cached by Riverpod for the lifetime of the provider scope. | ||
| final appVersionProvider = FutureProvider<String>((_) async { | ||
| final info = await PackageInfo.fromPlatform(); | ||
| return info.version; | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
_bootstrap(),await _ref.read(appVersionProvider.future)is called outside of anytry-catchblock. IfappVersionProviderthrows an exception (for example, ifPackageInfo.fromPlatform()fails on an unsupported platform or during unit tests), the exception will be unhandled because_bootstrap()is called asynchronously in the constructor without error handling.Wrapping this call in a
try-catchblock ensures the provider initializes gracefully even if the platform channel fails.