diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index dbafb948..3d8fcdf0 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
+
diff --git a/lib/main.dart b/lib/main.dart
index 52e59ec4..c5c7d5f6 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -18,6 +18,9 @@ import 'utils/location_permission.dart';
import 'services/notification_service.dart';
import 'services/notification_bridge.dart';
import 'services/background_location_service.dart';
+import 'services/app_update_service.dart';
+import 'widgets/update_dialog.dart';
+import 'utils/navigation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
@@ -594,6 +597,23 @@ Future main() async {
),
);
+ // 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 {
+ try {
+ final info = await AppUpdateService.instance.checkForUpdate();
+ if (info.isUpdateAvailable) {
+ showDialog(
+ context: globalNavigatorKey.currentContext!,
+ barrierDismissible: !info.isForceUpdate,
+ builder: (_) => UpdateDialog(info: info),
+ );
+ }
+ } catch (e) {
+ debugPrint('update check failed: $e');
+ }
+ });
+
// Post-startup registration removed: token registration is handled
// centrally in the auth state change listener to avoid duplicate inserts.
}
diff --git a/lib/models/app_version.dart b/lib/models/app_version.dart
new file mode 100644
index 00000000..86d8c31b
--- /dev/null
+++ b/lib/models/app_version.dart
@@ -0,0 +1,59 @@
+/// Represents a row in the ``app_versions`` table on the server. The
+/// `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;
+
+ /// 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;
+
+ /// A publicly‑accessible URL pointing at an APK stored in Supabase Storage.
+ final String downloadUrl;
+
+ /// Markdown/plaintext release notes that will be shown in the dialog.
+ final String releaseNotes;
+
+ AppVersion({
+ required this.versionCode,
+ required this.minVersionRequired,
+ required this.downloadUrl,
+ required this.releaseNotes,
+ });
+
+ factory AppVersion.fromMap(Map map) {
+ return AppVersion(
+ versionCode: map['version_code'] as int,
+ minVersionRequired: map['min_version_required'] as int,
+ downloadUrl: map['download_url'] as String,
+ releaseNotes: map['release_notes'] as String? ?? '',
+ );
+ }
+}
+
+/// Helper type returned by ``AppUpdateService.checkForUpdate`` so callers can
+/// decide how to present the UI.
+class AppUpdateInfo {
+ /// The version that was returned from ``app_versions``. ``null`` when the
+ /// table was empty (should not happen in normal operation).
+ final AppVersion? latestVersion;
+
+ /// Current build number as reported by ``package_info_plus``.
+ final int currentBuildNumber;
+
+ /// ``true`` when ``currentBuildNumber < latestVersion.versionCode``.
+ final bool isUpdateAvailable;
+
+ /// ``true`` when ``currentBuildNumber < latestVersion.minVersionRequired``
+ /// (regardless of whether there is a newer release available).
+ final bool isForceUpdate;
+
+ AppUpdateInfo({
+ required this.currentBuildNumber,
+ required this.latestVersion,
+ required this.isUpdateAvailable,
+ required this.isForceUpdate,
+ });
+}
diff --git a/lib/routing/app_router.dart b/lib/routing/app_router.dart
index f2c8ee06..1e49a994 100644
--- a/lib/routing/app_router.dart
+++ b/lib/routing/app_router.dart
@@ -11,6 +11,7 @@ import '../screens/auth/signup_screen.dart';
import '../screens/admin/offices_screen.dart';
import '../screens/admin/user_management_screen.dart';
import '../screens/admin/geofence_test_screen.dart';
+import '../screens/admin/app_update_screen.dart';
import '../screens/dashboard/dashboard_screen.dart';
import '../screens/notifications/notifications_screen.dart';
import '../screens/profile/profile_screen.dart';
@@ -31,11 +32,14 @@ import '../screens/it_service_requests/it_service_requests_list_screen.dart';
import '../screens/it_service_requests/it_service_request_detail_screen.dart';
import '../theme/m3_motion.dart';
+import '../utils/navigation.dart';
+
final appRouterProvider = Provider((ref) {
final notifier = RouterNotifier(ref);
ref.onDispose(notifier.dispose);
return GoRouter(
+ navigatorKey: globalNavigatorKey,
initialLocation: '/dashboard',
refreshListenable: notifier,
redirect: (context, state) {
@@ -104,6 +108,13 @@ final appRouterProvider = Provider((ref) {
child: const TeamsScreen(),
),
),
+ GoRoute(
+ path: '/settings/app-update',
+ pageBuilder: (context, state) => M3SharedAxisPage(
+ key: state.pageKey,
+ child: const AppUpdateScreen(),
+ ),
+ ),
GoRoute(
path: '/dashboard',
pageBuilder: (context, state) => M3SharedAxisPage(
diff --git a/lib/screens/admin/app_update_screen.dart b/lib/screens/admin/app_update_screen.dart
new file mode 100644
index 00000000..d4e02183
--- /dev/null
+++ b/lib/screens/admin/app_update_screen.dart
@@ -0,0 +1,284 @@
+import 'dart:async';
+import 'package:flutter/foundation.dart';
+
+import 'package:flutter/material.dart';
+import 'package:file_picker/file_picker.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:supabase_flutter/supabase_flutter.dart';
+
+/// A simple admin-only web page allowing the upload of a new APK and the
+/// associated metadata. After the APK is uploaded to the "apk_updates"
+/// bucket the `app_versions` table is updated and any older rows are removed
+/// so that only the current entry remains.
+class AppUpdateScreen extends ConsumerStatefulWidget {
+ const AppUpdateScreen({super.key});
+
+ @override
+ ConsumerState createState() => _AppUpdateScreenState();
+}
+
+class _AppUpdateScreenState extends ConsumerState {
+ final _formKey = GlobalKey();
+ final _versionController = TextEditingController();
+ final _minController = TextEditingController();
+ final _notesController = TextEditingController();
+
+ Uint8List? _apkBytes;
+ String? _apkName;
+ bool _isUploading = false;
+ double _progress = 0.0; // 0..1
+ String? _eta;
+ final List _logs = [];
+ Timer? _progressTimer;
+ String? _error;
+
+ @override
+ void initState() {
+ super.initState();
+ _loadCurrent();
+ }
+
+ 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'] ?? '';
+ }
+ } catch (_) {}
+ }
+
+ @override
+ void dispose() {
+ _versionController.dispose();
+ _minController.dispose();
+ _notesController.dispose();
+ super.dispose();
+ }
+
+ Future _pickApk() async {
+ final result = await FilePicker.platform.pickFiles(
+ type: FileType.custom,
+ allowedExtensions: ['apk'],
+ );
+ if (result != null && result.files.single.bytes != null) {
+ setState(() {
+ _apkBytes = result.files.single.bytes;
+ _apkName = result.files.single.name;
+ });
+ }
+ }
+
+ Future _submit() async {
+ if (!_formKey.currentState!.validate()) return;
+ if (_apkBytes == null || _apkName == null) {
+ setState(() => _error = 'Please select an APK file.');
+ return;
+ }
+
+ final vcode = _versionController.text.trim();
+ final minReq = _minController.text.trim();
+ final notes = _notesController.text;
+
+ setState(() {
+ _isUploading = true;
+ _progress = 0.0;
+ _eta = null;
+ _logs.clear();
+ _error = null;
+ });
+
+ try {
+ final client = Supabase.instance.client;
+ // ensure the user is authenticated (browser uploads require correct auth/CORS)
+ final user = client.auth.currentUser;
+ _logs.add('Current user: ${user?.id ?? 'anonymous'}');
+ if (user == null) {
+ throw Exception('Not signed in. Please sign in to perform uploads.');
+ }
+
+ // Use a deterministic object name to avoid accidental nesting
+ final filename = '${DateTime.now().millisecondsSinceEpoch}_$_apkName';
+ final path = filename;
+ _logs.add('Starting upload to bucket apk_updates, path: $path');
+ final stopwatch = Stopwatch()..start();
+ // we cannot track real progress from the SDK, so fake a linear update
+ final estimatedSeconds = (_apkBytes!.length / 250000).clamp(1, 30);
+ _progressTimer = Timer.periodic(const Duration(milliseconds: 200), (t) {
+ final elapsed = stopwatch.elapsed.inMilliseconds / 1000.0;
+ setState(() {
+ _progress = (elapsed / estimatedSeconds).clamp(0.0, 0.9);
+ final remaining = (estimatedSeconds - elapsed).clamp(
+ 0.0,
+ double.infinity,
+ );
+ _eta = '${remaining.toStringAsFixed(1)}s';
+ });
+ });
+
+ final uploadRes = await client.storage
+ .from('apk_updates')
+ .uploadBinary(
+ path,
+ _apkBytes!,
+ fileOptions: const FileOptions(
+ upsert: true,
+ contentType: 'application/vnd.android.package-archive',
+ ),
+ );
+ stopwatch.stop();
+ _logs.add('Upload finished (took ${stopwatch.elapsed.inSeconds}s)');
+ _logs.add('Raw upload response: ${uploadRes.runtimeType} - $uploadRes');
+ if (uploadRes is Map) {
+ final Map m = uploadRes as Map;
+ if (m.containsKey('error') && m['error'] != null) {
+ throw Exception('upload failed: ${m['error']}');
+ }
+ }
+ setState(() {
+ _progress = 0.95;
+ });
+ // retrieve public URL; various SDK versions return different structures
+ dynamic urlRes = client.storage.from('apk_updates').getPublicUrl(path);
+ _logs.add('Raw getPublicUrl response: ${urlRes.runtimeType} - $urlRes');
+ String url;
+ if (urlRes is String) {
+ url = urlRes;
+ } else if (urlRes is Map) {
+ // supabase responses vary by SDK version
+ if (urlRes['publicUrl'] is String) {
+ url = urlRes['publicUrl'] as String;
+ } else if (urlRes['data'] is String) {
+ url = urlRes['data'] as String;
+ } else if (urlRes['data'] is Map) {
+ final d = urlRes['data'] as Map;
+ url =
+ (d['publicUrl'] ?? d['public_url'] ?? d['url'] ?? d['publicURL'])
+ as String? ??
+ '';
+ } else {
+ url = '';
+ }
+ } else {
+ url = '';
+ }
+ 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({
+ 'version_code': vcode,
+ 'min_version_required': minReq,
+ 'download_url': url,
+ 'release_notes': notes,
+ }, onConflict: 'version_code');
+ await client.from('app_versions').delete().neq('version_code', vcode);
+ setState(() {
+ _progress = 1.0;
+ });
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Version saved successfully')),
+ );
+ }
+ } catch (e) {
+ _logs.add('Error during upload: $e');
+ setState(() => _error = e.toString());
+ } finally {
+ _progressTimer?.cancel();
+ _progressTimer = null;
+ setState(() => _isUploading = false);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (!kIsWeb) {
+ return const Center(
+ child: Text('This page is only available on the web.'),
+ );
+ }
+
+ return Scaffold(
+ appBar: AppBar(title: const Text('APK Update Uploader')),
+ body: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Form(
+ key: _formKey,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ TextFormField(
+ controller: _versionController,
+ decoration: const InputDecoration(
+ labelText: 'Version (e.g. 1.2.3)',
+ ),
+ keyboardType: TextInputType.text,
+ validator: (v) => (v == null || v.isEmpty) ? 'Required' : null,
+ ),
+ TextFormField(
+ controller: _minController,
+ decoration: const InputDecoration(
+ labelText: 'Min Version (e.g. 0.1.1)',
+ ),
+ keyboardType: TextInputType.text,
+ validator: (v) => (v == null || v.isEmpty) ? 'Required' : null,
+ ),
+ TextFormField(
+ controller: _notesController,
+ decoration: const InputDecoration(labelText: 'Release Notes'),
+ maxLines: 3,
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ ElevatedButton(
+ onPressed: _isUploading ? null : _pickApk,
+ child: const Text('Select APK'),
+ ),
+ const SizedBox(width: 8),
+ Expanded(child: Text(_apkName ?? 'no file chosen')),
+ ],
+ ),
+ const SizedBox(height: 20),
+ if (_isUploading) ...[
+ LinearProgressIndicator(value: _progress),
+ if (_eta != null)
+ Padding(
+ padding: const EdgeInsets.only(top: 4.0),
+ child: Text('ETA: $_eta'),
+ ),
+ const SizedBox(height: 12),
+ ConstrainedBox(
+ constraints: const BoxConstraints(maxHeight: 150),
+ child: ListView(
+ shrinkWrap: true,
+ children: _logs.map((l) => Text(l)).toList(),
+ ),
+ ),
+ const SizedBox(height: 12),
+ ],
+ if (_error != null)
+ Text(_error!, style: const TextStyle(color: Colors.red)),
+ ElevatedButton(
+ onPressed: _isUploading ? null : _submit,
+ child: _isUploading
+ ? const CircularProgressIndicator()
+ : const Text('Save'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/services/app_update_service.dart b/lib/services/app_update_service.dart
new file mode 100644
index 00000000..271e9281
--- /dev/null
+++ b/lib/services/app_update_service.dart
@@ -0,0 +1,136 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:ota_update/ota_update.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:supabase_flutter/supabase_flutter.dart';
+
+import '../models/app_version.dart';
+
+/// Handles the business logic for fetching version information from the
+/// ``app_versions`` table and downloading/installing a replacement APK.
+///
+/// This class is intentionally *not* coupled to any UI framework so that it can
+/// be unit‑tested easily. Widgets should only call into the public methods and
+/// display whatever progress / error states they need.
+class AppUpdateService {
+ AppUpdateService._();
+
+ /// Public singleton that other parts of the app can depend on. The private
+ /// constructor prevents instantiation from tests; they should mock the
+ /// individual methods instead.
+ static final instance = AppUpdateService._();
+
+ final SupabaseClient _client = Supabase.instance.client;
+
+ /// Fetches the most recent record from ``app_versions``. The table is
+ /// expected to contain a single row per release; we use the highest
+ /// ``version_code`` so that historical entries may be kept if desired.
+ Future _fetchLatestVersion() async {
+ try {
+ final Map? data = await _client
+ .from('app_versions')
+ .select()
+ .order('version_code', ascending: false)
+ .limit(1)
+ .maybeSingle();
+ if (data == null) return null;
+ return AppVersion.fromMap(data);
+ } catch (e) {
+ // rethrow so callers can handle/log
+ rethrow;
+ }
+ }
+
+ /// Compare the running build number with the latest data from the server and
+ /// return an [AppUpdateInfo] object describing the relationship. Errors
+ /// (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
+ return AppUpdateInfo(
+ currentBuildNumber: currentBuild,
+ latestVersion: null,
+ isUpdateAvailable: false,
+ isForceUpdate: false,
+ );
+ }
+
+ final available = currentBuild < serverVersion.versionCode;
+ final force = currentBuild < serverVersion.minVersionRequired;
+
+ return AppUpdateInfo(
+ currentBuildNumber: currentBuild,
+ latestVersion: serverVersion,
+ isUpdateAvailable: available,
+ isForceUpdate: force,
+ );
+ }
+
+ /// Downloads and installs the APK at [downloadUrl] using `ota_update`.
+ /// Progress is surfaced via [onProgress] (0.0–1.0). The returned future
+ /// completes once the download has finished and installation has started.
+ ///
+ /// Throws on permission denial or when the OTA stream reports an error.
+ Future downloadAndInstallApk(
+ String downloadUrl, {
+ required void Function(double) onProgress,
+ }) async {
+ if (!Platform.isAndroid) {
+ throw UnsupportedError(
+ 'In‑app updates currently only supported on Android',
+ );
+ }
+
+ final status = await Permission.requestInstallPackages.request();
+ if (!status.isGranted) {
+ throw Exception('installation permission denied');
+ }
+
+ final completer = Completer();
+ StreamSubscription? sub;
+ try {
+ sub = OtaUpdate()
+ .execute(
+ downloadUrl,
+ destinationFilename:
+ 'app_${DateTime.now().millisecondsSinceEpoch}.apk',
+ )
+ .listen(
+ (event) {
+ switch (event.status) {
+ case OtaStatus.DOWNLOADING:
+ final pct = double.tryParse(event.value ?? '0') ?? 0;
+ onProgress(pct / 100.0);
+ break;
+ case OtaStatus.INSTALLING:
+ if (!completer.isCompleted) completer.complete();
+ break;
+ default:
+ // treat all other statuses as failure; they provide a string
+ // description in `event.value`.
+ if (!completer.isCompleted) {
+ completer.completeError(
+ Exception('OTA update failed: ${event.status}'),
+ );
+ }
+ break;
+ }
+ },
+ onError: (e) {
+ if (!completer.isCompleted) completer.completeError(e);
+ },
+ );
+ await completer.future;
+ } finally {
+ if (sub != null) await sub.cancel();
+ }
+ }
+}
diff --git a/lib/utils/navigation.dart b/lib/utils/navigation.dart
new file mode 100644
index 00000000..f6a724b8
--- /dev/null
+++ b/lib/utils/navigation.dart
@@ -0,0 +1,6 @@
+import 'package:flutter/widgets.dart';
+
+/// Application-wide navigator key used for dialogs and external navigation
+/// triggers (e.g. background message handlers, startup checks).
+final GlobalKey globalNavigatorKey =
+ GlobalKey();
diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart
index 2e485fbe..062cf1a6 100644
--- a/lib/widgets/app_shell.dart
+++ b/lib/widgets/app_shell.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:flutter/foundation.dart' show kIsWeb;
import '../theme/m3_motion.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -439,6 +440,14 @@ List _buildSections(String role) {
icon: Icons.lock_open,
selectedIcon: Icons.lock,
),
+ if (kIsWeb) ...[
+ NavItem(
+ label: 'App Update',
+ route: '/settings/app-update',
+ icon: Icons.system_update_alt,
+ selectedIcon: Icons.system_update,
+ ),
+ ],
NavItem(
label: 'Logout',
route: '',
diff --git a/lib/widgets/update_dialog.dart b/lib/widgets/update_dialog.dart
new file mode 100644
index 00000000..1768317d
--- /dev/null
+++ b/lib/widgets/update_dialog.dart
@@ -0,0 +1,112 @@
+import 'package:flutter/material.dart';
+
+import '../models/app_version.dart';
+import '../services/app_update_service.dart';
+
+/// A reusable dialog that can render both flexible and forced updates and
+/// report download progress. Callers should wrap this with `showDialog` and
+/// control ``barrierDismissible`` according to ``info.isForceUpdate``.
+class UpdateDialog extends StatefulWidget {
+ final AppUpdateInfo info;
+
+ const UpdateDialog({required this.info, super.key});
+
+ @override
+ State createState() => _UpdateDialogState();
+}
+
+class _UpdateDialogState extends State {
+ double _progress = 0;
+ bool _downloading = false;
+ bool _failed = false;
+
+ Future _startDownload() async {
+ setState(() {
+ _downloading = true;
+ _failed = false;
+ });
+
+ try {
+ await AppUpdateService.instance.downloadAndInstallApk(
+ widget.info.latestVersion!.downloadUrl,
+ onProgress: (p) => setState(() => _progress = p),
+ );
+ // once the installer launches the app is likely to be stopped; we
+ // don't pop the dialog explicitly.
+ } catch (err) {
+ if (!mounted) return;
+ setState(() {
+ _failed = true;
+ _downloading = false;
+ });
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text('Download failed: $err')));
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final notes = widget.info.latestVersion?.releaseNotes ?? '';
+
+ // WillPopScope is deprecated in newer Flutter versions but PopScope
+ // has a different API; to avoid breaking changes we continue to use the
+ // old widget and suppress the warning.
+ // ignore: deprecated_member_use
+ return WillPopScope(
+ onWillPop: () async {
+ // prevent the user from dismissing when download is in progress or
+ // when the dialog is forcing an update
+ return !widget.info.isForceUpdate && !_downloading;
+ },
+ child: AlertDialog(
+ title: const Text('Update Available'),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (notes.isNotEmpty) ...[Text(notes), const SizedBox(height: 12)],
+ if (_downloading)
+ Column(
+ children: [
+ LinearProgressIndicator(value: _progress),
+ const SizedBox(height: 8),
+ Text('${(_progress * 100).toStringAsFixed(0)}%'),
+ ],
+ ),
+ if (_failed)
+ const Text(
+ 'An error occurred while downloading. Please try again.',
+ style: TextStyle(color: Colors.red),
+ ),
+ ],
+ ),
+ actions: _buildActions(),
+ ),
+ );
+ }
+
+ List _buildActions() {
+ if (_downloading) {
+ // don't show any actions while the apk is being fetched
+ return [];
+ }
+
+ if (widget.info.isForceUpdate) {
+ return [
+ FilledButton(
+ onPressed: _startDownload,
+ child: const Text('Update Now'),
+ ),
+ ];
+ }
+
+ return [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: const Text('Later'),
+ ),
+ FilledButton(onPressed: _startDownload, child: const Text('Update Now')),
+ ];
+ }
+}
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 23acd1c1..bf999e9d 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -15,6 +15,7 @@ import firebase_messaging
import flutter_image_compress_macos
import flutter_local_notifications
import geolocator_apple
+import package_info_plus
import printing
import quill_native_bridge_macos
import shared_preferences_foundation
@@ -31,6 +32,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
+ FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
diff --git a/pubspec.lock b/pubspec.lock
index a6462d2b..4660a5a6 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -353,6 +353,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.1"
+ dio:
+ dependency: "direct main"
+ description:
+ name: dio
+ sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.9.2"
+ dio_web_adapter:
+ dependency: transitive
+ description:
+ name: dio_web_adapter
+ sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
ed25519_edwards:
dependency: transitive
description:
@@ -1165,8 +1181,32 @@ packages:
url: "https://pub.dev"
source: hosted
version: "9.3.0"
- path:
+ ota_update:
+ dependency: "direct main"
+ description:
+ name: ota_update
+ sha256: "1f4c7c3c4f306729a6c00b84435096ce2d8b28439013f7237173acc699b2abc8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.1.0"
+ package_info_plus:
+ dependency: "direct main"
+ description:
+ name: package_info_plus
+ sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.0.0"
+ package_info_plus_platform_interface:
dependency: transitive
+ description:
+ name: package_info_plus_platform_interface
+ sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.1"
+ path:
+ dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
@@ -1182,7 +1222,7 @@ packages:
source: hosted
version: "1.1.0"
path_provider:
- dependency: transitive
+ dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
diff --git a/pubspec.yaml b/pubspec.yaml
index 5c8b6bed..80287827 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -37,18 +37,22 @@ dependencies:
shared_preferences: ^2.2.0
uuid: ^4.1.0
skeletonizer: ^2.1.3
+ path: ^1.8.0
+ path_provider: ^2.0.0
fl_chart: ^0.70.2
google_generative_ai: ^0.4.0
http: ^1.2.0
flutter_background_service: ^5.0.12
flutter_background_service_android: ^6.2.7
-
intl: ^0.20.2
image_picker: ^1.1.2
flutter_liveness_check: ^1.0.3
google_mlkit_face_detection: ^0.13.2
qr_flutter: ^4.1.0
flutter_image_compress: ^2.4.0
+ dio: ^5.1.2
+ package_info_plus: ^9.0.0
+ ota_update: ^7.1.0
dev_dependencies:
flutter_test: