A more robust self hosted OTA updates implementation

This commit is contained in:
2026-03-13 07:15:28 +08:00
parent 9267ebee2c
commit 9bbaf67fef
8 changed files with 852 additions and 164 deletions
+55 -15
View File
@@ -4,12 +4,14 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdfrx/pdfrx.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'screens/update_check_screen.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'firebase_options.dart';
// removed unused imports
import 'app.dart';
import 'theme/app_theme.dart';
import 'providers/notifications_provider.dart';
import 'providers/notification_navigation_provider.dart';
import 'utils/app_time.dart';
@@ -19,6 +21,7 @@ import 'services/notification_service.dart';
import 'services/notification_bridge.dart';
import 'services/background_location_service.dart';
import 'services/app_update_service.dart';
import 'models/app_version.dart';
import 'widgets/update_dialog.dart';
import 'utils/navigation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
@@ -593,29 +596,66 @@ Future<void> main() async {
runApp(
UncontrolledProviderScope(
container: _globalProviderContainer,
child: const NotificationBridge(child: TasqApp()),
child: const UpdateCheckWrapper(),
),
);
// perform update check once the first frame has rendered; errors are
// intentionally swallowed so a network outage doesn't block startup.
WidgetsBinding.instance.addPostFrameCallback((_) async {
// Post-startup registration removed: token registration is handled
// centrally in the auth state change listener to avoid duplicate inserts.
}
/// Wrapper shown at app launch; performs update check and displays
/// [UpdateCheckingScreen] until complete.
class UpdateCheckWrapper extends StatefulWidget {
const UpdateCheckWrapper({super.key});
@override
State<UpdateCheckWrapper> createState() => _UpdateCheckWrapperState();
}
class _UpdateCheckWrapperState extends State<UpdateCheckWrapper> {
bool _done = false;
AppUpdateInfo? _info;
@override
void initState() {
super.initState();
_performCheck();
}
Future<void> _performCheck() async {
try {
final info = await AppUpdateService.instance.checkForUpdate();
if (info.isUpdateAvailable) {
showDialog(
context: globalNavigatorKey.currentContext!,
barrierDismissible: !info.isForceUpdate,
builder: (_) => UpdateDialog(info: info),
);
}
_info = await AppUpdateService.instance.checkForUpdate();
} catch (e) {
debugPrint('update check failed: $e');
}
});
if (mounted) {
setState(() => _done = true);
if (_info?.isUpdateAvailable == true) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog(
context: globalNavigatorKey.currentContext!,
barrierDismissible: !_info!.isForceUpdate,
builder: (_) => UpdateDialog(info: _info!),
);
});
}
}
}
// Post-startup registration removed: token registration is handled
// centrally in the auth state change listener to avoid duplicate inserts.
@override
Widget build(BuildContext context) {
if (!_done) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
themeMode: ThemeMode.system,
home: const UpdateCheckingScreen(),
);
}
return const NotificationBridge(child: TasqApp());
}
}
class NotificationSoundObserver extends ProviderObserver {